@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.js
CHANGED
|
@@ -1756,7 +1756,18 @@ var init_constants = __esm({
|
|
|
1756
1756
|
"MINIMAX_API_KEY",
|
|
1757
1757
|
"ANTHROPIC_API_KEY",
|
|
1758
1758
|
"NVIDIA_API_KEY",
|
|
1759
|
-
"DEEPSEEK_API_KEY"
|
|
1759
|
+
"DEEPSEEK_API_KEY",
|
|
1760
|
+
// Run-scoped callback capabilities. The parent runner injects these after
|
|
1761
|
+
// sanitization; customer variables must never impersonate the control plane.
|
|
1762
|
+
"AUTH_FILE",
|
|
1763
|
+
"SHARED_AUTH_INJECT",
|
|
1764
|
+
"VALIDATE_RUN_ID",
|
|
1765
|
+
"VALIDATE_CLAIM_EPOCH",
|
|
1766
|
+
"VALIDATE_AUTH_CONFIG_HASH",
|
|
1767
|
+
"VALIDATE_TASK_ARN",
|
|
1768
|
+
"VALIDATE_RUNNER_AUTH",
|
|
1769
|
+
"VALIDATE_SERVER_URL",
|
|
1770
|
+
"VALIDATE_LIVE_BROWSER_STATE_PATH"
|
|
1760
1771
|
]);
|
|
1761
1772
|
SPAWNED_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
1762
1773
|
// Runtime essentials - Node.js and Playwright need these to function
|
|
@@ -4768,6 +4779,204 @@ var init_origin_domain = __esm({
|
|
|
4768
4779
|
}
|
|
4769
4780
|
});
|
|
4770
4781
|
|
|
4782
|
+
// ../shared/dist/auth-plan.js
|
|
4783
|
+
function hasInlineLoginSteps(playwrightCode) {
|
|
4784
|
+
if (!playwrightCode)
|
|
4785
|
+
return false;
|
|
4786
|
+
return /process\.env\.TEST_USER_EMAIL!?/.test(playwrightCode) && /process\.env\.TEST_USER_PASSWORD!?/.test(playwrightCode);
|
|
4787
|
+
}
|
|
4788
|
+
function hasGeneratedAuthGuard(playwrightCode) {
|
|
4789
|
+
if (!playwrightCode)
|
|
4790
|
+
return false;
|
|
4791
|
+
return /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
|
|
4792
|
+
}
|
|
4793
|
+
function legacyNeedsCookieState(tags, code, sharedAuthInjectEnabled2) {
|
|
4794
|
+
if (tags.includes("@auth"))
|
|
4795
|
+
return false;
|
|
4796
|
+
if (tags.includes("@api"))
|
|
4797
|
+
return true;
|
|
4798
|
+
if (!tags.includes("@needs-auth"))
|
|
4799
|
+
return false;
|
|
4800
|
+
if (sharedAuthInjectEnabled2 && tags.includes("@auth-guarded") && hasGeneratedAuthGuard(code))
|
|
4801
|
+
return true;
|
|
4802
|
+
const isGeneratedVariant = tags.includes("@edge") || tags.includes("@bug");
|
|
4803
|
+
const hasLegacyInlineLogin = tags.includes("@discovered") && !isGeneratedVariant && hasInlineLoginSteps(code);
|
|
4804
|
+
return !tags.includes("@inline-login") && !hasLegacyInlineLogin;
|
|
4805
|
+
}
|
|
4806
|
+
function explicitAuthTypeTags(t) {
|
|
4807
|
+
const found = /* @__PURE__ */ new Set();
|
|
4808
|
+
for (const tag of t) {
|
|
4809
|
+
if (AUTH_TYPE_TAGS.includes(tag))
|
|
4810
|
+
found.add(tag);
|
|
4811
|
+
else if (AUTH_TAG_ALIASES[tag])
|
|
4812
|
+
found.add(AUTH_TAG_ALIASES[tag]);
|
|
4813
|
+
}
|
|
4814
|
+
return [...found];
|
|
4815
|
+
}
|
|
4816
|
+
function invalidPlan(configurationError) {
|
|
4817
|
+
return {
|
|
4818
|
+
version: 1,
|
|
4819
|
+
source: "invalid",
|
|
4820
|
+
acquisition: "unsupported",
|
|
4821
|
+
presentation: "none",
|
|
4822
|
+
needsCookieState: false,
|
|
4823
|
+
needsStandingHeaders: false,
|
|
4824
|
+
standingHeadersMode: "none",
|
|
4825
|
+
needsLoginCreds: false,
|
|
4826
|
+
requiredEnvKeys: [],
|
|
4827
|
+
configurationError
|
|
4828
|
+
};
|
|
4829
|
+
}
|
|
4830
|
+
function explicitPlan(authTypeTag2, acquisition, presentation, opts) {
|
|
4831
|
+
return {
|
|
4832
|
+
version: 1,
|
|
4833
|
+
source: "explicit",
|
|
4834
|
+
authTypeTag: authTypeTag2,
|
|
4835
|
+
acquisition,
|
|
4836
|
+
presentation,
|
|
4837
|
+
needsCookieState: opts.cookie,
|
|
4838
|
+
needsStandingHeaders: opts.standingMode === "required",
|
|
4839
|
+
standingHeadersMode: opts.standingMode,
|
|
4840
|
+
needsLoginCreds: TEST_USER_ENV_KEYS.every((k) => opts.requiredEnvKeys.includes(k)),
|
|
4841
|
+
requiredEnvKeys: opts.requiredEnvKeys
|
|
4842
|
+
};
|
|
4843
|
+
}
|
|
4844
|
+
function deriveAuthPlan(tags, code, opts) {
|
|
4845
|
+
const t = Array.isArray(tags) ? tags : [];
|
|
4846
|
+
const sharedFlag = opts?.sharedAuthInjectEnabled ?? false;
|
|
4847
|
+
const isApi = t.includes("@api");
|
|
4848
|
+
const explicit = isApi ? explicitAuthTypeTags(t) : [];
|
|
4849
|
+
if (explicit.length > 0) {
|
|
4850
|
+
if (t.includes("@auth")) {
|
|
4851
|
+
return invalidPlan("@auth (self-login UI) cannot be combined with an explicit @api auth-type tag");
|
|
4852
|
+
}
|
|
4853
|
+
if (explicit.length > 1) {
|
|
4854
|
+
return invalidPlan(`conflicting auth-type tags: ${explicit.join(", ")}`);
|
|
4855
|
+
}
|
|
4856
|
+
switch (explicit[0]) {
|
|
4857
|
+
case AUTH_TAG_UNSUPPORTED:
|
|
4858
|
+
return {
|
|
4859
|
+
...invalidPlan("auth scheme not yet drivable (e.g. HMAC / mTLS / provider-replay) \u2014 needs manual configuration")
|
|
4860
|
+
};
|
|
4861
|
+
case AUTH_TAG_NONE:
|
|
4862
|
+
return explicitPlan(AUTH_TAG_NONE, "none", "none", { cookie: false, standingMode: "none", requiredEnvKeys: [] });
|
|
4863
|
+
case AUTH_TAG_STANDING:
|
|
4864
|
+
return explicitPlan(AUTH_TAG_STANDING, "standing-secret", "standing-header", { cookie: false, standingMode: "required", requiredEnvKeys: [API_AUTH_HEADERS_ENV_KEY] });
|
|
4865
|
+
case AUTH_TAG_LOGIN_BEARER:
|
|
4866
|
+
return explicitPlan(AUTH_TAG_LOGIN_BEARER, "request-login", "bearer", { cookie: false, standingMode: "none", requiredEnvKeys: [...TEST_USER_ENV_KEYS] });
|
|
4867
|
+
case AUTH_TAG_LOGIN_COOKIE:
|
|
4868
|
+
return explicitPlan(AUTH_TAG_LOGIN_COOKIE, "request-login", "cookie", { cookie: false, standingMode: "legacy-if-configured", requiredEnvKeys: [...TEST_USER_ENV_KEYS] });
|
|
4869
|
+
case AUTH_TAG_COOKIE_STATE:
|
|
4870
|
+
return explicitPlan(AUTH_TAG_COOKIE_STATE, "browser-bootstrap", "cookie", { cookie: true, standingMode: "legacy-if-configured", requiredEnvKeys: [...TEST_USER_ENV_KEYS] });
|
|
4871
|
+
}
|
|
4872
|
+
}
|
|
4873
|
+
const cookie = legacyNeedsCookieState(t, code, sharedFlag);
|
|
4874
|
+
const authedLegacy = t.includes("@auth") || t.includes("@needs-auth") || isApi;
|
|
4875
|
+
return {
|
|
4876
|
+
version: 1,
|
|
4877
|
+
source: "legacy",
|
|
4878
|
+
acquisition: cookie ? "browser-bootstrap" : authedLegacy ? "request-login" : "none",
|
|
4879
|
+
presentation: authedLegacy ? "cookie" : "none",
|
|
4880
|
+
needsCookieState: cookie,
|
|
4881
|
+
needsStandingHeaders: false,
|
|
4882
|
+
// A legacy bare-@api test keeps injecting standing headers IF the project configured them
|
|
4883
|
+
// (preserving today's behavior) — but does not require them. Non-@api legacy tests never inject.
|
|
4884
|
+
standingHeadersMode: isApi ? "legacy-if-configured" : "none",
|
|
4885
|
+
needsLoginCreds: authedLegacy,
|
|
4886
|
+
requiredEnvKeys: authedLegacy ? [...TEST_USER_ENV_KEYS] : []
|
|
4887
|
+
};
|
|
4888
|
+
}
|
|
4889
|
+
function authTypeTag(tags) {
|
|
4890
|
+
const t = Array.isArray(tags) ? tags : [];
|
|
4891
|
+
const explicit = explicitAuthTypeTags(t);
|
|
4892
|
+
return explicit.length === 1 ? explicit[0] : void 0;
|
|
4893
|
+
}
|
|
4894
|
+
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;
|
|
4895
|
+
var init_auth_plan = __esm({
|
|
4896
|
+
"../shared/dist/auth-plan.js"() {
|
|
4897
|
+
"use strict";
|
|
4898
|
+
AUTH_TAG_COOKIE_STATE = "@auth-cookie-state";
|
|
4899
|
+
AUTH_TAG_LOGIN_COOKIE = "@auth-login-cookie";
|
|
4900
|
+
AUTH_TAG_LOGIN_BEARER = "@auth-login-bearer";
|
|
4901
|
+
AUTH_TAG_STANDING = "@auth-standing";
|
|
4902
|
+
AUTH_TAG_NONE = "@auth-none";
|
|
4903
|
+
AUTH_TAG_UNSUPPORTED = "@auth-unsupported";
|
|
4904
|
+
AUTH_TYPE_TAGS = [
|
|
4905
|
+
AUTH_TAG_COOKIE_STATE,
|
|
4906
|
+
AUTH_TAG_LOGIN_COOKIE,
|
|
4907
|
+
AUTH_TAG_LOGIN_BEARER,
|
|
4908
|
+
AUTH_TAG_STANDING,
|
|
4909
|
+
AUTH_TAG_NONE,
|
|
4910
|
+
AUTH_TAG_UNSUPPORTED
|
|
4911
|
+
];
|
|
4912
|
+
AUTH_TAG_ALIASES = {
|
|
4913
|
+
"@auth-cookie": AUTH_TAG_COOKIE_STATE,
|
|
4914
|
+
"@auth-bearer": AUTH_TAG_LOGIN_BEARER,
|
|
4915
|
+
"@auth-token": AUTH_TAG_LOGIN_BEARER,
|
|
4916
|
+
"@auth-api-key": AUTH_TAG_STANDING,
|
|
4917
|
+
"@auth-standing-header": AUTH_TAG_STANDING
|
|
4918
|
+
};
|
|
4919
|
+
TEST_USER_ENV_KEYS = ["TEST_USER_EMAIL", "TEST_USER_PASSWORD"];
|
|
4920
|
+
API_AUTH_HEADERS_ENV_KEY = "API_AUTH_HEADERS";
|
|
4921
|
+
}
|
|
4922
|
+
});
|
|
4923
|
+
|
|
4924
|
+
// ../shared/dist/redact-needles.js
|
|
4925
|
+
function escapeRegExp(s) {
|
|
4926
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4927
|
+
}
|
|
4928
|
+
function buildNeedleMatcher(needles) {
|
|
4929
|
+
const usable = Array.from(new Set(needles.filter((n) => typeof n === "string" && n.length >= MIN_NEEDLE_LENGTH))).sort((a, b) => b.length - a.length);
|
|
4930
|
+
if (usable.length === 0)
|
|
4931
|
+
return null;
|
|
4932
|
+
return new RegExp(usable.map(escapeRegExp).join("|"), "g");
|
|
4933
|
+
}
|
|
4934
|
+
function redactNeedlesText(text, matcher) {
|
|
4935
|
+
if (!matcher || !text)
|
|
4936
|
+
return text;
|
|
4937
|
+
matcher.lastIndex = 0;
|
|
4938
|
+
const out = text.replace(matcher, REDACTED);
|
|
4939
|
+
return out === text ? text : out;
|
|
4940
|
+
}
|
|
4941
|
+
function redactNeedlesDeep(value, matcher) {
|
|
4942
|
+
if (!matcher)
|
|
4943
|
+
return value;
|
|
4944
|
+
if (typeof value === "string") {
|
|
4945
|
+
const scrubbed = redactNeedlesText(value, matcher);
|
|
4946
|
+
return scrubbed === value ? value : scrubbed;
|
|
4947
|
+
}
|
|
4948
|
+
if (Array.isArray(value)) {
|
|
4949
|
+
let changed = false;
|
|
4950
|
+
const next = value.map((item) => {
|
|
4951
|
+
const scrubbed = redactNeedlesDeep(item, matcher);
|
|
4952
|
+
if (scrubbed !== item)
|
|
4953
|
+
changed = true;
|
|
4954
|
+
return scrubbed;
|
|
4955
|
+
});
|
|
4956
|
+
return changed ? next : value;
|
|
4957
|
+
}
|
|
4958
|
+
if (value !== null && typeof value === "object") {
|
|
4959
|
+
let changed = false;
|
|
4960
|
+
const next = {};
|
|
4961
|
+
for (const [k, v] of Object.entries(value)) {
|
|
4962
|
+
const scrubbed = redactNeedlesDeep(v, matcher);
|
|
4963
|
+
if (scrubbed !== v)
|
|
4964
|
+
changed = true;
|
|
4965
|
+
next[k] = scrubbed;
|
|
4966
|
+
}
|
|
4967
|
+
return changed ? next : value;
|
|
4968
|
+
}
|
|
4969
|
+
return value;
|
|
4970
|
+
}
|
|
4971
|
+
var REDACTED, MIN_NEEDLE_LENGTH;
|
|
4972
|
+
var init_redact_needles = __esm({
|
|
4973
|
+
"../shared/dist/redact-needles.js"() {
|
|
4974
|
+
"use strict";
|
|
4975
|
+
REDACTED = "[REDACTED]";
|
|
4976
|
+
MIN_NEEDLE_LENGTH = 8;
|
|
4977
|
+
}
|
|
4978
|
+
});
|
|
4979
|
+
|
|
4771
4980
|
// ../shared/dist/voice-config.js
|
|
4772
4981
|
function normalizeTtsVoiceId(voiceId) {
|
|
4773
4982
|
if (!voiceId)
|
|
@@ -5024,6 +5233,16 @@ var init_voice_config = __esm({
|
|
|
5024
5233
|
}
|
|
5025
5234
|
});
|
|
5026
5235
|
|
|
5236
|
+
// ../shared/dist/feature-flags.js
|
|
5237
|
+
function isSharedAuthInjectEnabled(value = process.env.SHARED_AUTH_INJECT) {
|
|
5238
|
+
return value?.trim().toLowerCase() !== "false";
|
|
5239
|
+
}
|
|
5240
|
+
var init_feature_flags = __esm({
|
|
5241
|
+
"../shared/dist/feature-flags.js"() {
|
|
5242
|
+
"use strict";
|
|
5243
|
+
}
|
|
5244
|
+
});
|
|
5245
|
+
|
|
5027
5246
|
// ../shared/dist/index.js
|
|
5028
5247
|
var dist_exports = {};
|
|
5029
5248
|
__export(dist_exports, {
|
|
@@ -5032,8 +5251,16 @@ __export(dist_exports, {
|
|
|
5032
5251
|
ADDON_DEFINITIONS: () => ADDON_DEFINITIONS,
|
|
5033
5252
|
ADMIN_EMAIL_LIMITS: () => ADMIN_EMAIL_LIMITS,
|
|
5034
5253
|
AI_CREDIT_MARGIN: () => AI_CREDIT_MARGIN,
|
|
5254
|
+
API_AUTH_HEADERS_ENV_KEY: () => API_AUTH_HEADERS_ENV_KEY,
|
|
5035
5255
|
API_PREFIX: () => API_PREFIX,
|
|
5036
5256
|
AUDIO_CHUNK_DURATION_MS: () => AUDIO_CHUNK_DURATION_MS,
|
|
5257
|
+
AUTH_TAG_COOKIE_STATE: () => AUTH_TAG_COOKIE_STATE,
|
|
5258
|
+
AUTH_TAG_LOGIN_BEARER: () => AUTH_TAG_LOGIN_BEARER,
|
|
5259
|
+
AUTH_TAG_LOGIN_COOKIE: () => AUTH_TAG_LOGIN_COOKIE,
|
|
5260
|
+
AUTH_TAG_NONE: () => AUTH_TAG_NONE,
|
|
5261
|
+
AUTH_TAG_STANDING: () => AUTH_TAG_STANDING,
|
|
5262
|
+
AUTH_TAG_UNSUPPORTED: () => AUTH_TAG_UNSUPPORTED,
|
|
5263
|
+
AUTH_TYPE_TAGS: () => AUTH_TYPE_TAGS,
|
|
5037
5264
|
COST_PER_M: () => COST_PER_M,
|
|
5038
5265
|
CREDIT_PACKS_CENTS: () => CREDIT_PACKS_CENTS,
|
|
5039
5266
|
CREDIT_PRICES_CENTS: () => CREDIT_PRICES_CENTS,
|
|
@@ -5076,6 +5303,7 @@ __export(dist_exports, {
|
|
|
5076
5303
|
STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
|
|
5077
5304
|
STT_PROVIDER_OPTIONS: () => STT_PROVIDER_OPTIONS,
|
|
5078
5305
|
SUPPORT_EMAIL: () => SUPPORT_EMAIL,
|
|
5306
|
+
TEST_USER_ENV_KEYS: () => TEST_USER_ENV_KEYS,
|
|
5079
5307
|
TRANSIENT_CONTROL_ROLES: () => TRANSIENT_CONTROL_ROLES,
|
|
5080
5308
|
TRIAL_DURATION_DAYS: () => TRIAL_DURATION_DAYS,
|
|
5081
5309
|
TTS_MODEL_OPTIONS: () => TTS_MODEL_OPTIONS,
|
|
@@ -5085,10 +5313,12 @@ __export(dist_exports, {
|
|
|
5085
5313
|
accumulateStatements: () => accumulateStatements,
|
|
5086
5314
|
aiCostToCreditCents: () => aiCostToCreditCents,
|
|
5087
5315
|
analyzeMobileSelectorQuality: () => analyzeMobileSelectorQuality,
|
|
5316
|
+
authTypeTag: () => authTypeTag,
|
|
5088
5317
|
bugFoundQuarantineWhere: () => bugFoundQuarantineWhere,
|
|
5089
5318
|
buildAppiumCode: () => buildAppiumCode,
|
|
5090
5319
|
buildLaunchStep: () => buildLaunchStep,
|
|
5091
5320
|
buildMobileStep: () => buildMobileStep,
|
|
5321
|
+
buildNeedleMatcher: () => buildNeedleMatcher,
|
|
5092
5322
|
buildSandboxedEnv: () => buildSandboxedEnv,
|
|
5093
5323
|
buildSanitizedAgentEnv: () => buildSanitizedAgentEnv,
|
|
5094
5324
|
buildStepTarget: () => buildStepTarget,
|
|
@@ -5104,6 +5334,7 @@ __export(dist_exports, {
|
|
|
5104
5334
|
controlSignature: () => controlSignature,
|
|
5105
5335
|
countInnerTests: () => countInnerTests,
|
|
5106
5336
|
decideHarPosted: () => decideHarPosted,
|
|
5337
|
+
deriveAuthPlan: () => deriveAuthPlan,
|
|
5107
5338
|
deriveProjectKey: () => deriveProjectKey,
|
|
5108
5339
|
detectCrossOriginHtmlAsJson: () => detectCrossOriginHtmlAsJson,
|
|
5109
5340
|
detectsErrorBannerAssertion: () => detectsErrorBannerAssertion,
|
|
@@ -5147,6 +5378,7 @@ __export(dist_exports, {
|
|
|
5147
5378
|
isRunnableStep: () => isRunnableStep,
|
|
5148
5379
|
isSafeDeepLink: () => isSafeDeepLink,
|
|
5149
5380
|
isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
|
|
5381
|
+
isSharedAuthInjectEnabled: () => isSharedAuthInjectEnabled,
|
|
5150
5382
|
isSttModelForProvider: () => isSttModelForProvider,
|
|
5151
5383
|
isTestReadyForPlatform: () => isTestReadyForPlatform,
|
|
5152
5384
|
isTrialExpired: () => isTrialExpired,
|
|
@@ -5170,6 +5402,8 @@ __export(dist_exports, {
|
|
|
5170
5402
|
planHasBillingPortal: () => planHasBillingPortal,
|
|
5171
5403
|
planRequiresCheckout: () => planRequiresCheckout,
|
|
5172
5404
|
platformTag: () => platformTag,
|
|
5405
|
+
redactNeedlesDeep: () => redactNeedlesDeep,
|
|
5406
|
+
redactNeedlesText: () => redactNeedlesText,
|
|
5173
5407
|
registrableDomain: () => registrableDomain,
|
|
5174
5408
|
resolveCrossOriginConfigOverride: () => resolveCrossOriginConfigOverride,
|
|
5175
5409
|
resolveMobileStepValuePlaceholders: () => resolveMobileStepValuePlaceholders,
|
|
@@ -5212,7 +5446,10 @@ var init_dist = __esm({
|
|
|
5212
5446
|
init_agent_env();
|
|
5213
5447
|
init_test_counting();
|
|
5214
5448
|
init_origin_domain();
|
|
5449
|
+
init_auth_plan();
|
|
5450
|
+
init_redact_needles();
|
|
5215
5451
|
init_voice_config();
|
|
5452
|
+
init_feature_flags();
|
|
5216
5453
|
}
|
|
5217
5454
|
});
|
|
5218
5455
|
|
|
@@ -10676,7 +10913,7 @@ var require_transport = __commonJS({
|
|
|
10676
10913
|
"../../node_modules/pino/lib/transport.js"(exports2, module2) {
|
|
10677
10914
|
"use strict";
|
|
10678
10915
|
var { createRequire } = require("module");
|
|
10679
|
-
var { existsSync:
|
|
10916
|
+
var { existsSync: existsSync3 } = require("fs");
|
|
10680
10917
|
var getCallers = require_caller();
|
|
10681
10918
|
var { join: join7, isAbsolute, sep } = require("path");
|
|
10682
10919
|
var { fileURLToPath } = require("url");
|
|
@@ -10750,7 +10987,7 @@ var require_transport = __commonJS({
|
|
|
10750
10987
|
return false;
|
|
10751
10988
|
}
|
|
10752
10989
|
}
|
|
10753
|
-
return isAbsolute(path) && !
|
|
10990
|
+
return isAbsolute(path) && !existsSync3(path);
|
|
10754
10991
|
}
|
|
10755
10992
|
function stripQuotes(value) {
|
|
10756
10993
|
const first = value[0];
|
|
@@ -52378,11 +52615,11 @@ var require_leac = __commonJS({
|
|
|
52378
52615
|
if (r.str.length === 0) {
|
|
52379
52616
|
throw new Error(`Rule #${i} ("${r.name}") has empty "str" property, which is not allowed.`);
|
|
52380
52617
|
}
|
|
52381
|
-
return new RegExp(
|
|
52618
|
+
return new RegExp(escapeRegExp2(r.str), "y");
|
|
52382
52619
|
}
|
|
52383
|
-
return new RegExp(
|
|
52620
|
+
return new RegExp(escapeRegExp2(r.name), "y");
|
|
52384
52621
|
}
|
|
52385
|
-
function
|
|
52622
|
+
function escapeRegExp2(str) {
|
|
52386
52623
|
return str.replace(/[-[\]{}()*+!<=:?./\\^$|#\s,]/g, "\\$&");
|
|
52387
52624
|
}
|
|
52388
52625
|
function toSticky(re) {
|
|
@@ -68355,7 +68592,7 @@ var require_discover_ai_planner = __commonJS({
|
|
|
68355
68592
|
"../runner-core/dist/services/discover-ai-planner.js"(exports2) {
|
|
68356
68593
|
"use strict";
|
|
68357
68594
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
68358
|
-
exports2.SCENARIO_SYSTEM_PROMPT = void 0;
|
|
68595
|
+
exports2.PLANNER_STRENGTHEN_GATE_PROMPT = exports2.SCENARIO_SYSTEM_PROMPT = void 0;
|
|
68359
68596
|
exports2.getAIPlannerMaxTokens = getAIPlannerMaxTokens;
|
|
68360
68597
|
exports2.getSnapshotConfidence = getSnapshotConfidence;
|
|
68361
68598
|
exports2.compressSiteMap = compressSiteMap;
|
|
@@ -68938,7 +69175,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68938
69175
|
4. **Multi-page journeys** - only when transitions were actually observed
|
|
68939
69176
|
5. **Bug reports** - for observed 500 errors, crashes, or broken features
|
|
68940
69177
|
6. **Steps are human-readable** - "Fill the email field", not "page.getByLabel('Email').fill()"
|
|
68941
|
-
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
|
|
69178
|
+
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.
|
|
68942
69179
|
8. **Auth-aware** - mark scenarios that need login. Use process.env.TEST_USER_EMAIL / PASSWORD
|
|
68943
69180
|
9. **No duplicates** - check existing test names before adding
|
|
68944
69181
|
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.
|
|
@@ -68951,7 +69188,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
68951
69188
|
Example - BAD happy path: \`["Navigate to /feature", "Click Create", "Verify dialog opens"]\` \u2192 only tests the trigger.
|
|
68952
69189
|
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"]\`
|
|
68953
69190
|
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"]\`
|
|
68954
|
-
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
|
|
69191
|
+
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.
|
|
68955
69192
|
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.
|
|
68956
69193
|
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>."
|
|
68957
69194
|
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.
|
|
@@ -69014,6 +69251,9 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
69014
69251
|
Citing observed labels in \`expectedOutcomes\` is fine \u2014 that's asserting on stable product signals, not implementation specifics.
|
|
69015
69252
|
|
|
69016
69253
|
## Coverage Strategy
|
|
69254
|
+
**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.
|
|
69255
|
+
|
|
69256
|
+
Apply the per-surface minimums below only where the evidence actually supports them (evidence support includes Rule 1's trigger-only structural exception):
|
|
69017
69257
|
- For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 11) + 1 EDGE_CASE (empty/invalid submission)
|
|
69018
69258
|
- 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)
|
|
69019
69259
|
- 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.
|
|
@@ -69043,11 +69283,11 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
|
|
|
69043
69283
|
{ "action": "Fill the email field with test credentials" },
|
|
69044
69284
|
{ "action": "Fill the password field with test credentials" },
|
|
69045
69285
|
{ "action": "Click the Sign In button" },
|
|
69046
|
-
{ "action": "
|
|
69286
|
+
{ "action": "After reaching the observed post-login page, reload it" }
|
|
69047
69287
|
],
|
|
69048
69288
|
"expectedOutcomes": [
|
|
69049
|
-
{ "type": "url", "description": "User reaches /dashboard after signing in", "reliability": "high" },
|
|
69050
|
-
{ "type": "element-
|
|
69289
|
+
{ "type": "url", "description": "User reaches /dashboard after signing in and remains there after reload", "reliability": "high" },
|
|
69290
|
+
{ "type": "element-hidden", "description": "The login form does not return after reload", "reliability": "medium" }
|
|
69051
69291
|
],
|
|
69052
69292
|
"targetPages": ["/auth/login", "/dashboard"],
|
|
69053
69293
|
"prerequisites": []
|
|
@@ -69077,6 +69317,46 @@ Each entry is exactly one of:
|
|
|
69077
69317
|
3 = medium (navigation, secondary features)
|
|
69078
69318
|
4 = low (edge cases, cosmetic)
|
|
69079
69319
|
5 = nice-to-have`;
|
|
69320
|
+
exports2.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:
|
|
69321
|
+
- The PLAN owns the ORACLE: WHAT must be true for the behavior to be correct, stated in plain English.
|
|
69322
|
+
- The GENERATOR owns BINDING: locators and the exact runtime values it reads off the live page.
|
|
69323
|
+
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.
|
|
69324
|
+
|
|
69325
|
+
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.
|
|
69326
|
+
|
|
69327
|
+
## Rule 0 \u2014 REPAIR OR DROP, never merely describe (highest priority)
|
|
69328
|
+
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.
|
|
69329
|
+
|
|
69330
|
+
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).
|
|
69331
|
+
|
|
69332
|
+
## Rule 1 \u2014 Classify each scenario, then require the matching oracle
|
|
69333
|
+
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:
|
|
69334
|
+
|
|
69335
|
+
- 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.
|
|
69336
|
+
- 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.
|
|
69337
|
+
- 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).
|
|
69338
|
+
- 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.
|
|
69339
|
+
- 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.
|
|
69340
|
+
- 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.
|
|
69341
|
+
- EXTERNAL INTEGRATION: assert the outbound URL/payload contains the expected parameters (drawn from evidence), not a frozen full string.
|
|
69342
|
+
- STATIC CONTENT: presence/label oracle is acceptable; do not inflate it. Verify shared chrome (nav/footer) ONCE, not per page.
|
|
69343
|
+
- 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.
|
|
69344
|
+
|
|
69345
|
+
## Rule 2 \u2014 Exact values: judge by ORIGIN, not specificity
|
|
69346
|
+
- 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").
|
|
69347
|
+
- 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.
|
|
69348
|
+
|
|
69349
|
+
## Rule 3 \u2014 Do not build on flows the evidence shows are broken or unrepeatable
|
|
69350
|
+
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.
|
|
69351
|
+
|
|
69352
|
+
## Rule 4 \u2014 Suite economy and completeness
|
|
69353
|
+
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.
|
|
69354
|
+
|
|
69355
|
+
## Rule 5 \u2014 reliability is honest
|
|
69356
|
+
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.
|
|
69357
|
+
|
|
69358
|
+
## Output
|
|
69359
|
+
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.`;
|
|
69080
69360
|
function coerceRawPrerequisites(raw) {
|
|
69081
69361
|
if (!Array.isArray(raw))
|
|
69082
69362
|
return [];
|
|
@@ -69860,6 +70140,95 @@ Each entry is exactly one of:
|
|
|
69860
70140
|
}
|
|
69861
70141
|
}
|
|
69862
70142
|
}
|
|
70143
|
+
const gateFlag = (process.env.PLANNER_STRENGTHEN_GATE || "").trim().toLowerCase();
|
|
70144
|
+
const gateEnabled = gateFlag !== "false" && gateFlag !== "0";
|
|
70145
|
+
if (gateEnabled && context.mode !== "survey" && !options.systemPromptOverride && !options.userPromptOverride && !extraPlannerCallUsed && scenarios.length > 0) {
|
|
70146
|
+
const gateModel = options.gateModelOverride ? (0, ai_provider_js_1.resolveModelForTask)(options.gateModelOverride, "agent") : agentModel;
|
|
70147
|
+
const gateClient = options.gateModelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(options.gateModelOverride) : aiClient;
|
|
70148
|
+
const gateMaxTokens = getAIPlannerMaxTokens(gateModel);
|
|
70149
|
+
const gatePlan = { featureGroups, scenarios, reasoning: aiOutput.reasoning ?? "" };
|
|
70150
|
+
const gateUser = `EVIDENCE (ground truth):
|
|
70151
|
+
${userPrompt}
|
|
70152
|
+
|
|
70153
|
+
=====
|
|
70154
|
+
|
|
70155
|
+
PLAN TO AUDIT AND REPAIR (JSON):
|
|
70156
|
+
${JSON.stringify(gatePlan)}`;
|
|
70157
|
+
const gateStart = Date.now();
|
|
70158
|
+
let gateResponse = null;
|
|
70159
|
+
try {
|
|
70160
|
+
extraPlannerCallUsed = true;
|
|
70161
|
+
gateResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
|
|
70162
|
+
arm();
|
|
70163
|
+
return gateClient.chat.completions.create({
|
|
70164
|
+
model: gateModel,
|
|
70165
|
+
messages: [
|
|
70166
|
+
{ role: "system", content: exports2.PLANNER_STRENGTHEN_GATE_PROMPT },
|
|
70167
|
+
{ role: "user", content: gateUser }
|
|
70168
|
+
],
|
|
70169
|
+
max_tokens: gateMaxTokens,
|
|
70170
|
+
temperature: 0.2,
|
|
70171
|
+
...(0, ai_provider_js_1.getJsonObjectResponseFormat)(gateModel),
|
|
70172
|
+
...(0, ai_provider_js_1.getNoThinkingRequestOptions)(gateModel)
|
|
70173
|
+
}, { signal });
|
|
70174
|
+
}), {
|
|
70175
|
+
label: "ai-planner-strengthen-gate",
|
|
70176
|
+
deferTimeoutUntilArmed: true,
|
|
70177
|
+
onRetry: (attempt, reason, delay) => logs2.push(`AI planner strengthen-gate call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
|
|
70178
|
+
});
|
|
70179
|
+
} catch (err) {
|
|
70180
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
70181
|
+
logs2.push(`AI planner strengthen-gate failed (${message}) \u2014 keeping pre-gate result`);
|
|
70182
|
+
}
|
|
70183
|
+
if (gateResponse) {
|
|
70184
|
+
try {
|
|
70185
|
+
options.onAICall?.({
|
|
70186
|
+
iteration: 1,
|
|
70187
|
+
groupId: `ai-planner-${context.projectId.slice(0, 8)}`,
|
|
70188
|
+
callType: "discoveryAIPlannerStrengthenGate",
|
|
70189
|
+
model: gateModel,
|
|
70190
|
+
provider: (0, ai_provider_js_1.getProviderForModel)(gateModel),
|
|
70191
|
+
durationMs: Date.now() - gateStart,
|
|
70192
|
+
promptTokens: gateResponse.usage?.prompt_tokens,
|
|
70193
|
+
completionTokens: gateResponse.usage?.completion_tokens,
|
|
70194
|
+
totalTokens: gateResponse.usage?.total_tokens,
|
|
70195
|
+
assistantMessage: gateResponse.choices[0]?.message?.content,
|
|
70196
|
+
requestMessageCount: 2
|
|
70197
|
+
});
|
|
70198
|
+
} catch {
|
|
70199
|
+
}
|
|
70200
|
+
const gateRaw = gateResponse.choices[0]?.message?.content?.trim();
|
|
70201
|
+
if (gateRaw) {
|
|
70202
|
+
let gateOut = null;
|
|
70203
|
+
try {
|
|
70204
|
+
gateOut = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(gateRaw));
|
|
70205
|
+
} catch (err) {
|
|
70206
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
70207
|
+
logs2.push(`AI planner strengthen-gate returned invalid JSON (${message}) \u2014 keeping pre-gate result`);
|
|
70208
|
+
}
|
|
70209
|
+
if (gateOut) {
|
|
70210
|
+
try {
|
|
70211
|
+
const vg = validateAIPlannerOutput(gateOut, siteMap, context.existingTestNames, context.testBudget, evidence.recordedJourneys, context.mode, context.focusAreas);
|
|
70212
|
+
const preGateCount = scenarios.length;
|
|
70213
|
+
const keepsCoverage = vg.scenarios.length >= Math.ceil(preGateCount * 0.5);
|
|
70214
|
+
const noWorseGaps = vg.coverageGaps.length <= coverageGaps.length;
|
|
70215
|
+
if (vg.scenarios.length > 0 && keepsCoverage && noWorseGaps) {
|
|
70216
|
+
logs2.push(`AI planner strengthen-gate [${gateModel}]: ${preGateCount}\u2192${vg.scenarios.length} scenarios, gaps ${coverageGaps.length}\u2192${vg.coverageGaps.length}; taking gated result`);
|
|
70217
|
+
scenarios = vg.scenarios;
|
|
70218
|
+
featureGroups = vg.featureGroups;
|
|
70219
|
+
warnings = [...warnings, ...vg.warnings];
|
|
70220
|
+
coverageGaps = vg.coverageGaps;
|
|
70221
|
+
} else {
|
|
70222
|
+
logs2.push(`AI planner strengthen-gate rejected (scen ${preGateCount}\u2192${vg.scenarios.length}, gaps ${coverageGaps.length}\u2192${vg.coverageGaps.length}); keeping pre-gate result`);
|
|
70223
|
+
}
|
|
70224
|
+
} catch (err) {
|
|
70225
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
70226
|
+
logs2.push(`AI planner strengthen-gate validation threw (${message}) \u2014 keeping pre-gate result`);
|
|
70227
|
+
}
|
|
70228
|
+
}
|
|
70229
|
+
}
|
|
70230
|
+
}
|
|
70231
|
+
}
|
|
69863
70232
|
const totalFinal = scenarios.length + coverageGaps.length;
|
|
69864
70233
|
const rejectionRateFinal = totalFinal > 0 ? coverageGaps.length / totalFinal : 0;
|
|
69865
70234
|
if (rejectionRateFinal > 0.5 && totalFinal >= 3) {
|
|
@@ -73639,7 +74008,7 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
73639
74008
|
// OpenAI-style secret key
|
|
73640
74009
|
];
|
|
73641
74010
|
var EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
|
|
73642
|
-
function
|
|
74011
|
+
function escapeRegExp2(value) {
|
|
73643
74012
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
73644
74013
|
}
|
|
73645
74014
|
function collectSecretVars(testVariables, minLen, keyRe) {
|
|
@@ -73658,7 +74027,7 @@ var require_mobile_heal_postprocess = __commonJS({
|
|
|
73658
74027
|
return text;
|
|
73659
74028
|
let out = text;
|
|
73660
74029
|
for (const { key, value } of collectSecretVars(testVariables, MIN_REDACT_LEN, REDACT_KEY_RE)) {
|
|
73661
|
-
out = out.replace(new RegExp(
|
|
74030
|
+
out = out.replace(new RegExp(escapeRegExp2(value), "g"), `{{${key}}}`);
|
|
73662
74031
|
}
|
|
73663
74032
|
for (const source of TOKEN_SOURCES) {
|
|
73664
74033
|
out = out.replace(new RegExp(source, "g"), "[REDACTED]");
|
|
@@ -76058,7 +76427,7 @@ var require_api_auth_headers = __commonJS({
|
|
|
76058
76427
|
"../runner-core/dist/services/api-auth-headers.js"(exports2) {
|
|
76059
76428
|
"use strict";
|
|
76060
76429
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
76061
|
-
exports2.parseApiAuthHeaders =
|
|
76430
|
+
exports2.parseApiAuthHeaders = parseApiAuthHeaders2;
|
|
76062
76431
|
exports2.resolveApiAuthHeaders = resolveApiAuthHeaders;
|
|
76063
76432
|
exports2.isAppOriginRequest = isAppOriginRequest;
|
|
76064
76433
|
exports2.buildOriginScopedApiFixtures = buildOriginScopedApiFixtures;
|
|
@@ -76071,7 +76440,7 @@ var require_api_auth_headers = __commonJS({
|
|
|
76071
76440
|
}
|
|
76072
76441
|
return false;
|
|
76073
76442
|
}
|
|
76074
|
-
function
|
|
76443
|
+
function parseApiAuthHeaders2(raw) {
|
|
76075
76444
|
if (!raw || typeof raw !== "string")
|
|
76076
76445
|
return void 0;
|
|
76077
76446
|
const trimmed = raw.trim();
|
|
@@ -76095,9 +76464,9 @@ var require_api_auth_headers = __commonJS({
|
|
|
76095
76464
|
function resolveApiAuthHeaders(input) {
|
|
76096
76465
|
if (input.explicit)
|
|
76097
76466
|
return input.explicit;
|
|
76098
|
-
if (
|
|
76467
|
+
if (input.standingHeadersMode === "none")
|
|
76099
76468
|
return void 0;
|
|
76100
|
-
return
|
|
76469
|
+
return parseApiAuthHeaders2(input.apiAuthHeadersVar);
|
|
76101
76470
|
}
|
|
76102
76471
|
function isAppOriginRequest(url, baseUrl) {
|
|
76103
76472
|
if (typeof url !== "string" || !baseUrl)
|
|
@@ -76165,6 +76534,126 @@ export const test = base.extend({
|
|
|
76165
76534
|
}
|
|
76166
76535
|
});
|
|
76167
76536
|
|
|
76537
|
+
// ../runner-core/dist/services/run-callback-proxy.js
|
|
76538
|
+
var require_run_callback_proxy = __commonJS({
|
|
76539
|
+
"../runner-core/dist/services/run-callback-proxy.js"(exports2) {
|
|
76540
|
+
"use strict";
|
|
76541
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
76542
|
+
exports2.startRunCallbackProxy = startRunCallbackProxy;
|
|
76543
|
+
var node_crypto_1 = require("crypto");
|
|
76544
|
+
var node_http_1 = require("http");
|
|
76545
|
+
var MAX_CALLBACK_BODY_BYTES = 5 * 1024 * 1024;
|
|
76546
|
+
var CALLBACK_TIMEOUT_MS = 3e4;
|
|
76547
|
+
function sendJson(res, status, body) {
|
|
76548
|
+
const payload = JSON.stringify(body);
|
|
76549
|
+
res.writeHead(status, {
|
|
76550
|
+
"cache-control": "no-store",
|
|
76551
|
+
"content-type": "application/json",
|
|
76552
|
+
"content-length": Buffer.byteLength(payload)
|
|
76553
|
+
});
|
|
76554
|
+
res.end(payload);
|
|
76555
|
+
}
|
|
76556
|
+
async function readRequestBody(req) {
|
|
76557
|
+
const chunks = [];
|
|
76558
|
+
let total = 0;
|
|
76559
|
+
for await (const chunk of req) {
|
|
76560
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
76561
|
+
total += buffer.length;
|
|
76562
|
+
if (total > MAX_CALLBACK_BODY_BYTES)
|
|
76563
|
+
throw new Error("callback body too large");
|
|
76564
|
+
chunks.push(buffer);
|
|
76565
|
+
}
|
|
76566
|
+
return Buffer.concat(chunks);
|
|
76567
|
+
}
|
|
76568
|
+
async function startRunCallbackProxy(input) {
|
|
76569
|
+
const upstream = new URL(input.serverUrl);
|
|
76570
|
+
if (upstream.protocol !== "http:" && upstream.protocol !== "https:") {
|
|
76571
|
+
throw new Error("callback server URL must use http or https");
|
|
76572
|
+
}
|
|
76573
|
+
const runPath = `/api/runner/runs/${encodeURIComponent(input.runId)}`;
|
|
76574
|
+
const allowedPaths = /* @__PURE__ */ new Set([`${runPath}/credential`, `${runPath}/auth-state`]);
|
|
76575
|
+
const token = (0, node_crypto_1.randomBytes)(32).toString("base64url");
|
|
76576
|
+
const authHeader = `Bearer ${token}`;
|
|
76577
|
+
const server3 = (0, node_http_1.createServer)(async (req, res) => {
|
|
76578
|
+
try {
|
|
76579
|
+
const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
76580
|
+
if (req.method !== "POST" || !allowedPaths.has(requestUrl.pathname) || requestUrl.search) {
|
|
76581
|
+
sendJson(res, 404, { error: "Callback route not available" });
|
|
76582
|
+
return;
|
|
76583
|
+
}
|
|
76584
|
+
if (req.headers.authorization !== authHeader) {
|
|
76585
|
+
sendJson(res, 401, { error: "Invalid callback capability" });
|
|
76586
|
+
return;
|
|
76587
|
+
}
|
|
76588
|
+
const body = await readRequestBody(req);
|
|
76589
|
+
let parsedBody;
|
|
76590
|
+
try {
|
|
76591
|
+
const parsed = JSON.parse(body.toString("utf8"));
|
|
76592
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
76593
|
+
throw new Error("invalid body");
|
|
76594
|
+
parsedBody = parsed;
|
|
76595
|
+
} catch {
|
|
76596
|
+
sendJson(res, 400, { error: "Callback body must be a JSON object" });
|
|
76597
|
+
return;
|
|
76598
|
+
}
|
|
76599
|
+
const upstreamBody = JSON.stringify({
|
|
76600
|
+
...parsedBody,
|
|
76601
|
+
...input.taskArn ? { taskArn: input.taskArn } : {},
|
|
76602
|
+
...input.claimEpoch !== void 0 ? { claimEpoch: input.claimEpoch } : {},
|
|
76603
|
+
...input.authConfigHash ? { authConfigHash: input.authConfigHash } : {}
|
|
76604
|
+
});
|
|
76605
|
+
const destination = new URL(requestUrl.pathname, upstream);
|
|
76606
|
+
const response = await fetch(destination, {
|
|
76607
|
+
method: "POST",
|
|
76608
|
+
headers: {
|
|
76609
|
+
Authorization: input.upstreamAuthHeader,
|
|
76610
|
+
"Content-Type": "application/json"
|
|
76611
|
+
},
|
|
76612
|
+
body: upstreamBody,
|
|
76613
|
+
redirect: "error",
|
|
76614
|
+
signal: AbortSignal.timeout(CALLBACK_TIMEOUT_MS)
|
|
76615
|
+
});
|
|
76616
|
+
const responseBody = Buffer.from(await response.arrayBuffer());
|
|
76617
|
+
res.writeHead(response.status, {
|
|
76618
|
+
"cache-control": "no-store",
|
|
76619
|
+
"content-type": response.headers.get("content-type") ?? "application/json",
|
|
76620
|
+
"content-length": responseBody.length
|
|
76621
|
+
});
|
|
76622
|
+
res.end(responseBody);
|
|
76623
|
+
} catch (error2) {
|
|
76624
|
+
const message = error2 instanceof Error && error2.message === "callback body too large" ? error2.message : "Callback forwarding failed";
|
|
76625
|
+
sendJson(res, message === "callback body too large" ? 413 : 502, { error: message });
|
|
76626
|
+
}
|
|
76627
|
+
});
|
|
76628
|
+
server3.on("clientError", (_error, socket) => socket.destroy());
|
|
76629
|
+
await new Promise((resolve2, reject) => {
|
|
76630
|
+
const onError = (error2) => reject(error2);
|
|
76631
|
+
server3.once("error", onError);
|
|
76632
|
+
server3.listen(0, "127.0.0.1", () => {
|
|
76633
|
+
server3.off("error", onError);
|
|
76634
|
+
resolve2();
|
|
76635
|
+
});
|
|
76636
|
+
});
|
|
76637
|
+
const address = server3.address();
|
|
76638
|
+
if (!address) {
|
|
76639
|
+
server3.close();
|
|
76640
|
+
throw new Error("callback proxy failed to bind");
|
|
76641
|
+
}
|
|
76642
|
+
let closed = false;
|
|
76643
|
+
return {
|
|
76644
|
+
serverUrl: `http://127.0.0.1:${address.port}`,
|
|
76645
|
+
authHeader,
|
|
76646
|
+
close: async () => {
|
|
76647
|
+
if (closed)
|
|
76648
|
+
return;
|
|
76649
|
+
closed = true;
|
|
76650
|
+
await new Promise((resolve2) => server3.close(() => resolve2()));
|
|
76651
|
+
}
|
|
76652
|
+
};
|
|
76653
|
+
}
|
|
76654
|
+
}
|
|
76655
|
+
});
|
|
76656
|
+
|
|
76168
76657
|
// ../runner-core/dist/services/playwright-native.js
|
|
76169
76658
|
var require_playwright_native = __commonJS({
|
|
76170
76659
|
"../runner-core/dist/services/playwright-native.js"(exports2) {
|
|
@@ -76209,6 +76698,7 @@ var require_playwright_native = __commonJS({
|
|
|
76209
76698
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
76210
76699
|
exports2.scrubHarRequestBody = exports2.buildOriginScopedApiFixtures = exports2.isAppOriginRequest = exports2.resolveApiAuthHeaders = exports2.parseApiAuthHeaders = void 0;
|
|
76211
76700
|
exports2.executeTestViaNativePlaywright = executeTestViaNativePlaywright2;
|
|
76701
|
+
exports2.buildNativePlaywrightChildEnv = buildNativePlaywrightChildEnv;
|
|
76212
76702
|
exports2.parseJsonResults = parseJsonResults2;
|
|
76213
76703
|
exports2.parseHarToApiSummary = parseHarToApiSummary;
|
|
76214
76704
|
var promises_1 = require("fs/promises");
|
|
@@ -76220,6 +76710,7 @@ var require_playwright_native = __commonJS({
|
|
|
76220
76710
|
var api_call_redaction_js_1 = require_api_call_redaction();
|
|
76221
76711
|
var har_stream_js_1 = require_har_stream();
|
|
76222
76712
|
var api_auth_headers_js_1 = require_api_auth_headers();
|
|
76713
|
+
var run_callback_proxy_js_1 = require_run_callback_proxy();
|
|
76223
76714
|
var api_auth_headers_js_2 = require_api_auth_headers();
|
|
76224
76715
|
Object.defineProperty(exports2, "parseApiAuthHeaders", { enumerable: true, get: function() {
|
|
76225
76716
|
return api_auth_headers_js_2.parseApiAuthHeaders;
|
|
@@ -76242,7 +76733,7 @@ var require_playwright_native = __commonJS({
|
|
|
76242
76733
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
76243
76734
|
var PW_CLI = require.resolve("@playwright/test/cli");
|
|
76244
76735
|
async function executeTestViaNativePlaywright2(options) {
|
|
76245
|
-
const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
76736
|
+
const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, authStandingMode, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
|
|
76246
76737
|
const logs2 = [];
|
|
76247
76738
|
const screenshots = [];
|
|
76248
76739
|
logs2.push(`\u{1F3AD} Native Playwright starting`);
|
|
@@ -76252,15 +76743,19 @@ var require_playwright_native = __commonJS({
|
|
|
76252
76743
|
logs2.push("\u26A0\uFE0F WARNING: baseUrl is empty - test will likely fail with navigation errors");
|
|
76253
76744
|
}
|
|
76254
76745
|
let runDir = null;
|
|
76746
|
+
let standingNeedleMatcher = null;
|
|
76747
|
+
let callbackProxy;
|
|
76255
76748
|
try {
|
|
76256
76749
|
runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
|
|
76257
76750
|
await (0, promises_1.mkdir)(runDir, { recursive: true });
|
|
76258
76751
|
const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
|
|
76752
|
+
const effectiveStandingMode = isApiTest2 ? authStandingMode ?? "legacy-if-configured" : "none";
|
|
76259
76753
|
const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
|
|
76260
76754
|
explicit: extraHttpHeaders,
|
|
76261
|
-
|
|
76755
|
+
standingHeadersMode: effectiveStandingMode,
|
|
76262
76756
|
apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
|
|
76263
76757
|
});
|
|
76758
|
+
standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)(resolvedExtraHeaders ? Object.values(resolvedExtraHeaders) : []);
|
|
76264
76759
|
const cssDisableSetup = `
|
|
76265
76760
|
const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
|
|
76266
76761
|
let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
|
|
@@ -76455,6 +76950,18 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76455
76950
|
logs2.push("\u26A0 Could not find @playwright/test import - animation disabling NOT injected");
|
|
76456
76951
|
}
|
|
76457
76952
|
const useScopedApiHeaders = isApiTest2 && !!resolvedExtraHeaders && !!importMatch && !!baseUrl;
|
|
76953
|
+
const authDelivery = { standingHeadersConfigured: !!resolvedExtraHeaders, standingHeadersInjected: useScopedApiHeaders };
|
|
76954
|
+
if (resolvedExtraHeaders && !useScopedApiHeaders) {
|
|
76955
|
+
logs2.push("\u26A0 Standing API auth headers were configured but could not be installed origin-scoped \u2014 failing closed (no global fallback).");
|
|
76956
|
+
return {
|
|
76957
|
+
passed: false,
|
|
76958
|
+
stepResults: [],
|
|
76959
|
+
screenshots: [],
|
|
76960
|
+
logs: logs2.join("\n"),
|
|
76961
|
+
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.',
|
|
76962
|
+
authDelivery
|
|
76963
|
+
};
|
|
76964
|
+
}
|
|
76458
76965
|
let instrumentedCode;
|
|
76459
76966
|
if (isApiTest2) {
|
|
76460
76967
|
instrumentedCode = useScopedApiHeaders ? playwrightCode.replace(importMatch[0], importMatch[0].replace(/from\s+['"]@playwright\/test['"]/, "from './qa-api-fixtures'")) : playwrightCode;
|
|
@@ -76478,7 +76985,7 @@ test.afterEach(async ({ page }, testInfo) => {
|
|
|
76478
76985
|
const absHarPath = wantHar ? (0, path_1.join)(runDir, "session.har").replace(/\\/g, "/") : null;
|
|
76479
76986
|
const recordHarLine = absHarPath ? `recordHar: { path: ${JSON.stringify(absHarPath)}, mode: 'full' },` : "";
|
|
76480
76987
|
const storageStateLine = storageStatePath ? `storageState: ${JSON.stringify(storageStatePath.replace(/\\/g, "/"))},` : "";
|
|
76481
|
-
const extraHeadersLine =
|
|
76988
|
+
const extraHeadersLine = "";
|
|
76482
76989
|
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'],";
|
|
76483
76990
|
const config = `
|
|
76484
76991
|
// Plain object export - no require('@playwright/test') needed.
|
|
@@ -76511,35 +77018,51 @@ module.exports = {
|
|
|
76511
77018
|
const configPath = (0, path_1.join)(runDir, "playwright.config.js");
|
|
76512
77019
|
await (0, promises_1.writeFile)(configPath, config);
|
|
76513
77020
|
const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(runDir);
|
|
76514
|
-
const
|
|
76515
|
-
|
|
77021
|
+
const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
|
|
77022
|
+
const callbackClaimEpoch = Number.isSafeInteger(validateContext?.claimEpoch) ? validateContext?.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
|
|
77023
|
+
if (validateContext?.runId && validateContext.serverUrl && validateContext.authHeader) {
|
|
77024
|
+
try {
|
|
77025
|
+
callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
|
|
77026
|
+
runId: validateContext.runId,
|
|
77027
|
+
serverUrl: validateContext.serverUrl,
|
|
77028
|
+
upstreamAuthHeader: validateContext.authHeader,
|
|
77029
|
+
taskArn: process.env.VALIDATE_TASK_ARN,
|
|
77030
|
+
claimEpoch: callbackClaimEpoch,
|
|
77031
|
+
authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
|
|
77032
|
+
});
|
|
77033
|
+
} catch (error3) {
|
|
77034
|
+
logs2.push(`Callback proxy unavailable: ${error3 instanceof Error ? error3.message : String(error3)}`);
|
|
77035
|
+
}
|
|
77036
|
+
}
|
|
77037
|
+
const childTestVariables = { ...testVariables ?? {} };
|
|
77038
|
+
const trustedRuntimeEnv = {
|
|
76516
77039
|
...isolation.env,
|
|
76517
|
-
//
|
|
76518
|
-
//
|
|
76519
|
-
|
|
76520
|
-
|
|
76521
|
-
|
|
76522
|
-
|
|
76523
|
-
|
|
77040
|
+
// Generated callbacks receive only a random loopback capability. The global
|
|
77041
|
+
// runner bearer token and real control-plane URL stay in the parent process.
|
|
77042
|
+
...callbackProxy && validateContext?.runId ? {
|
|
77043
|
+
VALIDATE_RUN_ID: validateContext.runId,
|
|
77044
|
+
...callbackClaimEpoch !== void 0 ? { VALIDATE_CLAIM_EPOCH: String(callbackClaimEpoch) } : {},
|
|
77045
|
+
VALIDATE_SERVER_URL: callbackProxy.serverUrl,
|
|
77046
|
+
VALIDATE_RUNNER_AUTH: callbackProxy.authHeader
|
|
77047
|
+
} : {},
|
|
76524
77048
|
// Owning Fargate taskArn (a per-task constant set once at task boot in
|
|
76525
77049
|
// execute-one.ts; absent on the local/Railway runner). Read from
|
|
76526
77050
|
// process.env rather than validateContext since it's process-wide, not
|
|
76527
77051
|
// per-run. Lets persistCredential() prove ownership to /credential.
|
|
76528
77052
|
...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {},
|
|
77053
|
+
...authStateCapturePath ? { AUTH_FILE: authStateCapturePath } : {},
|
|
76529
77054
|
...liveBrowserStatePath ? { VALIDATE_LIVE_BROWSER_STATE_PATH: liveBrowserStatePath } : {},
|
|
76530
|
-
//
|
|
76531
|
-
//
|
|
76532
|
-
//
|
|
76533
|
-
// this forward the flag reads as undefined in the test and the early publish is dead
|
|
76534
|
-
// code. Forwarded (not allowlisted) so it stays a per-run toggle; absent → inert.
|
|
77055
|
+
// Preserve an explicit shared-auth kill-switch in the child. Unset defaults on in
|
|
77056
|
+
// both parent and generated __vqaLogin code; "false" must still cross this process
|
|
77057
|
+
// boundary to disable the early publish POST. It remains a per-run toggle.
|
|
76535
77058
|
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
|
|
76536
77059
|
};
|
|
76537
|
-
delete
|
|
77060
|
+
delete childTestVariables.API_AUTH_HEADERS;
|
|
76538
77061
|
logs2.push("\u{1F680} Spawning playwright test...");
|
|
76539
77062
|
const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
|
|
76540
77063
|
let playResult;
|
|
76541
77064
|
try {
|
|
76542
|
-
playResult = await runPlaywright(configPath,
|
|
77065
|
+
playResult = await runPlaywright(configPath, childTestVariables, trustedRuntimeEnv);
|
|
76543
77066
|
} finally {
|
|
76544
77067
|
await stopLiveBrowserPolling?.();
|
|
76545
77068
|
}
|
|
@@ -76600,7 +77123,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
76600
77123
|
logs2.push(`\u26A0 HAR parse skipped: ${msg.slice(0, 200)}`);
|
|
76601
77124
|
}
|
|
76602
77125
|
}
|
|
76603
|
-
|
|
77126
|
+
const nativeResult = {
|
|
76604
77127
|
passed,
|
|
76605
77128
|
stepResults,
|
|
76606
77129
|
screenshots,
|
|
@@ -76609,20 +77132,27 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
76609
77132
|
harApiCalls,
|
|
76610
77133
|
logs: logs2.join("\n"),
|
|
76611
77134
|
error: passed ? void 0 : error2,
|
|
76612
|
-
suspiciousPass
|
|
77135
|
+
suspiciousPass,
|
|
77136
|
+
authDelivery
|
|
76613
77137
|
};
|
|
77138
|
+
if (!standingNeedleMatcher)
|
|
77139
|
+
return nativeResult;
|
|
77140
|
+
return (0, shared_1.redactNeedlesDeep)(nativeResult, standingNeedleMatcher);
|
|
76614
77141
|
} catch (fatalErr) {
|
|
76615
77142
|
const msg = fatalErr instanceof Error ? fatalErr.message : String(fatalErr);
|
|
76616
77143
|
logs2.push(`
|
|
76617
77144
|
\u{1F4A5} Fatal error: ${msg}`);
|
|
76618
|
-
|
|
77145
|
+
const fatalResult = {
|
|
76619
77146
|
passed: false,
|
|
76620
77147
|
stepResults: [],
|
|
76621
77148
|
screenshots,
|
|
76622
77149
|
logs: logs2.join("\n"),
|
|
76623
77150
|
error: msg
|
|
76624
77151
|
};
|
|
77152
|
+
return standingNeedleMatcher ? (0, shared_1.redactNeedlesDeep)(fatalResult, standingNeedleMatcher) : fatalResult;
|
|
76625
77153
|
} finally {
|
|
77154
|
+
await callbackProxy?.close().catch(() => {
|
|
77155
|
+
});
|
|
76626
77156
|
if (runDir) {
|
|
76627
77157
|
await (0, promises_1.rm)(runDir, { recursive: true, force: true }).catch(() => {
|
|
76628
77158
|
});
|
|
@@ -76674,9 +77204,9 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
76674
77204
|
stopped = true;
|
|
76675
77205
|
};
|
|
76676
77206
|
}
|
|
76677
|
-
function runPlaywright(configPath,
|
|
77207
|
+
function runPlaywright(configPath, testVariables, trustedRuntimeEnv) {
|
|
76678
77208
|
return new Promise((resolvePromise) => {
|
|
76679
|
-
const sandboxedEnv = (
|
|
77209
|
+
const sandboxedEnv = buildNativePlaywrightChildEnv(process.env, testVariables, trustedRuntimeEnv);
|
|
76680
77210
|
const proc = (0, spawn_with_tree_kill_js_1.spawnDetachedGroup)(process.execPath, [PW_CLI, "test", "--config", configPath], {
|
|
76681
77211
|
cwd: PACKAGE_DIR,
|
|
76682
77212
|
env: sandboxedEnv,
|
|
@@ -76719,6 +77249,12 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
|
|
|
76719
77249
|
});
|
|
76720
77250
|
});
|
|
76721
77251
|
}
|
|
77252
|
+
function buildNativePlaywrightChildEnv(processEnv, testVariables, trustedRuntimeEnv) {
|
|
77253
|
+
return {
|
|
77254
|
+
...(0, shared_1.buildSandboxedEnv)(processEnv, testVariables),
|
|
77255
|
+
...trustedRuntimeEnv ?? {}
|
|
77256
|
+
};
|
|
77257
|
+
}
|
|
76722
77258
|
async function collectScreenshots(dir, screenshots) {
|
|
76723
77259
|
let entries;
|
|
76724
77260
|
try {
|
|
@@ -80440,10 +80976,12 @@ var require_auth_recipe = __commonJS({
|
|
|
80440
80976
|
exports2.upsertAuthBlock = upsertAuthBlock;
|
|
80441
80977
|
exports2.buildHealAuthStamp = buildHealAuthStamp;
|
|
80442
80978
|
var crypto_1 = require("crypto");
|
|
80979
|
+
var shared_1 = (init_dist(), __toCommonJS(dist_exports));
|
|
80443
80980
|
var discovery_lint_flags_js_1 = require_discovery_lint_flags();
|
|
80444
|
-
exports2.AUTH_MARKER_VERSION = "
|
|
80981
|
+
exports2.AUTH_MARKER_VERSION = "v3";
|
|
80445
80982
|
var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
|
|
80446
80983
|
var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} <<<`;
|
|
80984
|
+
var SHARED_AUTH_RUNTIME_ENABLED = `!/^false$/i.test((process.env.SHARED_AUTH_INJECT ?? '').trim())`;
|
|
80447
80985
|
var AUTH_REGION_RE = /\/\/ >>> VALIDATE_QA_AUTH[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH[^\n]*\r?\n?/g;
|
|
80448
80986
|
function computeRecipeHash(input) {
|
|
80449
80987
|
const canonical = JSON.stringify({
|
|
@@ -80521,16 +81059,18 @@ var require_auth_recipe = __commonJS({
|
|
|
80521
81059
|
if (opts?.warmGuard) {
|
|
80522
81060
|
const warmLanding = recipe.redirectRoute ?? (recipe.authProof.kind === "url" ? recipe.authProof.route : recipe.preNavRoute);
|
|
80523
81061
|
const loginRoute = recipe.route ?? recipe.preNavRoute;
|
|
80524
|
-
lines.push(`
|
|
81062
|
+
lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED}) {`);
|
|
81063
|
+
lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
|
|
80525
81064
|
if (recipe.authProof.kind === "url") {
|
|
80526
81065
|
const pred = urlProofPredicateExpr(recipe.authProof.route);
|
|
80527
|
-
lines.push(`
|
|
80528
|
-
lines.push(`
|
|
81066
|
+
lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
|
|
81067
|
+
lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
|
|
80529
81068
|
} else if (loginRoute) {
|
|
80530
|
-
lines.push(`
|
|
81069
|
+
lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
|
|
80531
81070
|
} else {
|
|
80532
|
-
lines.push(`
|
|
81071
|
+
lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
|
|
80533
81072
|
}
|
|
81073
|
+
lines.push(` }`);
|
|
80534
81074
|
}
|
|
80535
81075
|
lines.push(` await page.goto(${JSON.stringify(recipe.preNavRoute)}, { waitUntil: 'domcontentloaded' });`);
|
|
80536
81076
|
for (const cmd of recipe.preNavCmds)
|
|
@@ -80543,11 +81083,11 @@ var require_auth_recipe = __commonJS({
|
|
|
80543
81083
|
lines.push(renderAuthUrlWaitLine(recipe.redirectRoute));
|
|
80544
81084
|
lines.push(` ${renderAuthProofAssertion(recipe.authProof)}`);
|
|
80545
81085
|
if (opts?.warmGuard) {
|
|
80546
|
-
lines.push(` if (process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
|
|
80547
|
-
lines.push(` if (process.env.
|
|
81086
|
+
lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
|
|
81087
|
+
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) {`);
|
|
80548
81088
|
lines.push(` try {`);
|
|
80549
81089
|
lines.push(` const __vqaState = await page.context().storageState();`);
|
|
80550
|
-
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 });`);
|
|
81090
|
+
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 });`);
|
|
80551
81091
|
lines.push(` } catch {}`);
|
|
80552
81092
|
lines.push(` }`);
|
|
80553
81093
|
}
|
|
@@ -80559,6 +81099,8 @@ var require_auth_recipe = __commonJS({
|
|
|
80559
81099
|
"AUTH_FILE",
|
|
80560
81100
|
"SHARED_AUTH_INJECT",
|
|
80561
81101
|
"VALIDATE_RUN_ID",
|
|
81102
|
+
"VALIDATE_CLAIM_EPOCH",
|
|
81103
|
+
"VALIDATE_AUTH_CONFIG_HASH",
|
|
80562
81104
|
"VALIDATE_TASK_ARN",
|
|
80563
81105
|
"VALIDATE_RUNNER_AUTH",
|
|
80564
81106
|
"VALIDATE_SERVER_URL"
|
|
@@ -80731,7 +81273,7 @@ ${tail}`;
|
|
|
80731
81273
|
let result = stripAuthRegions(code);
|
|
80732
81274
|
result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
|
|
80733
81275
|
result = ensureExpectImported(result);
|
|
80734
|
-
const warmGuard =
|
|
81276
|
+
const warmGuard = (0, shared_1.isSharedAuthInjectEnabled)();
|
|
80735
81277
|
result = insertAfterPwImport(result, renderAuthFn(ctx.recipe, { warmGuard }));
|
|
80736
81278
|
result = ensureLoginCall(result);
|
|
80737
81279
|
const n = countAuthRegions(result);
|
|
@@ -85711,7 +86253,7 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
|
|
|
85711
86253
|
}
|
|
85712
86254
|
return result;
|
|
85713
86255
|
}
|
|
85714
|
-
function
|
|
86256
|
+
function escapeRegExp2(value) {
|
|
85715
86257
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
85716
86258
|
}
|
|
85717
86259
|
function buildSensitiveEnvReplacements(testVariables) {
|
|
@@ -85733,7 +86275,7 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
|
|
|
85733
86275
|
for (const replacement of buildSensitiveEnvReplacements(testVariables)) {
|
|
85734
86276
|
if (replacement.value.includes("\n") || replacement.value.includes("\r"))
|
|
85735
86277
|
continue;
|
|
85736
|
-
const literalPattern = new RegExp(`(['"\`])${
|
|
86278
|
+
const literalPattern = new RegExp(`(['"\`])${escapeRegExp2(replacement.value)}\\1`, "g");
|
|
85737
86279
|
result = result.replace(literalPattern, replacement.envRef);
|
|
85738
86280
|
}
|
|
85739
86281
|
return result;
|
|
@@ -85861,9 +86403,9 @@ ${testOpen}`;
|
|
|
85861
86403
|
basePath: options?.basePath
|
|
85862
86404
|
});
|
|
85863
86405
|
const leakedSensitiveKeys = findSensitiveLiteralLeaks(playwrightCode, options?.sensitiveEnvMap);
|
|
85864
|
-
const authGuardStamped = !!options?.authRecipe && rawTags.includes("@needs-auth") && !rawTags.includes("@auth") && process
|
|
85865
|
-
const baseTags = ensureDiscoveredTag(Array.isArray(test.tags) ? test.tags : ["@discovered", "@regression"]);
|
|
85866
|
-
const tags = authGuardStamped
|
|
86406
|
+
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);
|
|
86407
|
+
const baseTags = ensureDiscoveredTag(Array.isArray(test.tags) ? test.tags : ["@discovered", "@regression"]).filter((tag) => tag !== "@auth-guarded");
|
|
86408
|
+
const tags = authGuardStamped ? [...baseTags, "@auth-guarded"] : baseTags;
|
|
85867
86409
|
return {
|
|
85868
86410
|
name: String(test.name || "Unnamed test").slice(0, 200),
|
|
85869
86411
|
description: String(test.description || test.name || "").slice(0, 500),
|
|
@@ -99678,7 +100220,7 @@ ${lanes.join("\n")}
|
|
|
99678
100220
|
return process.memoryUsage().heapUsed;
|
|
99679
100221
|
},
|
|
99680
100222
|
getFileSize(path) {
|
|
99681
|
-
const stat =
|
|
100223
|
+
const stat = statSync(path);
|
|
99682
100224
|
if (stat == null ? void 0 : stat.isFile()) {
|
|
99683
100225
|
return stat.size;
|
|
99684
100226
|
}
|
|
@@ -99722,7 +100264,7 @@ ${lanes.join("\n")}
|
|
|
99722
100264
|
}
|
|
99723
100265
|
};
|
|
99724
100266
|
return nodeSystem;
|
|
99725
|
-
function
|
|
100267
|
+
function statSync(path) {
|
|
99726
100268
|
try {
|
|
99727
100269
|
return _fs.statSync(path, statSyncOptions);
|
|
99728
100270
|
} catch {
|
|
@@ -99781,7 +100323,7 @@ ${lanes.join("\n")}
|
|
|
99781
100323
|
activeSession.post("Profiler.stop", (err, { profile }) => {
|
|
99782
100324
|
var _a;
|
|
99783
100325
|
if (!err) {
|
|
99784
|
-
if ((_a =
|
|
100326
|
+
if ((_a = statSync(profilePath)) == null ? void 0 : _a.isDirectory()) {
|
|
99785
100327
|
profilePath = _path.join(profilePath, `${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`);
|
|
99786
100328
|
}
|
|
99787
100329
|
try {
|
|
@@ -99901,7 +100443,7 @@ ${lanes.join("\n")}
|
|
|
99901
100443
|
let stat;
|
|
99902
100444
|
if (typeof dirent === "string" || dirent.isSymbolicLink()) {
|
|
99903
100445
|
const name = combinePaths(path, entry);
|
|
99904
|
-
stat =
|
|
100446
|
+
stat = statSync(name);
|
|
99905
100447
|
if (!stat) {
|
|
99906
100448
|
continue;
|
|
99907
100449
|
}
|
|
@@ -99925,7 +100467,7 @@ ${lanes.join("\n")}
|
|
|
99925
100467
|
return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames2, process.cwd(), depth, getAccessibleFileSystemEntries, realpath);
|
|
99926
100468
|
}
|
|
99927
100469
|
function fileSystemEntryExists(path, entryKind) {
|
|
99928
|
-
const stat =
|
|
100470
|
+
const stat = statSync(path);
|
|
99929
100471
|
if (!stat) {
|
|
99930
100472
|
return false;
|
|
99931
100473
|
}
|
|
@@ -99967,7 +100509,7 @@ ${lanes.join("\n")}
|
|
|
99967
100509
|
}
|
|
99968
100510
|
function getModifiedTime3(path) {
|
|
99969
100511
|
var _a;
|
|
99970
|
-
return (_a =
|
|
100512
|
+
return (_a = statSync(path)) == null ? void 0 : _a.mtime;
|
|
99971
100513
|
}
|
|
99972
100514
|
function setModifiedTime(path, time) {
|
|
99973
100515
|
try {
|
|
@@ -254989,7 +255531,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
|
|
|
254989
255531
|
}
|
|
254990
255532
|
}
|
|
254991
255533
|
return result;
|
|
254992
|
-
function
|
|
255534
|
+
function escapeRegExp2(str) {
|
|
254993
255535
|
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
|
254994
255536
|
}
|
|
254995
255537
|
function getTodoCommentsRegExp() {
|
|
@@ -254997,7 +255539,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
|
|
|
254997
255539
|
const multiLineCommentStart = /(?:\/\*+\s*)/.source;
|
|
254998
255540
|
const anyNumberOfSpacesAndAsterisksAtStartOfLine = /(?:^(?:\s|\*)*)/.source;
|
|
254999
255541
|
const preamble = "(" + anyNumberOfSpacesAndAsterisksAtStartOfLine + "|" + singleLineCommentStart + "|" + multiLineCommentStart + ")";
|
|
255000
|
-
const literals = "(?:" + map2(descriptors, (d) => "(" +
|
|
255542
|
+
const literals = "(?:" + map2(descriptors, (d) => "(" + escapeRegExp2(d.text) + ")").join("|") + ")";
|
|
255001
255543
|
const endOfLineOrEndOfComment = /(?:$|\*\/)/.source;
|
|
255002
255544
|
const messageRemainder = /(?:.*?)/.source;
|
|
255003
255545
|
const messagePortion = "(" + literals + messageRemainder + ")";
|
|
@@ -303257,7 +303799,7 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
|
|
|
303257
303799
|
confidence: "low"
|
|
303258
303800
|
};
|
|
303259
303801
|
}
|
|
303260
|
-
function
|
|
303802
|
+
function escapeRegExp2(s) {
|
|
303261
303803
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
303262
303804
|
}
|
|
303263
303805
|
function tryDeterministicStrictModeFix(code, error2) {
|
|
@@ -303280,10 +303822,10 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
|
|
|
303280
303822
|
const [, , role, , name, restOfOptions] = violated;
|
|
303281
303823
|
if (/exact\s*:\s*true/.test(restOfOptions))
|
|
303282
303824
|
return null;
|
|
303283
|
-
const akaPattern = new RegExp(`aka\\s+getByRole\\(\\s*(['"\`])${
|
|
303825
|
+
const akaPattern = new RegExp(`aka\\s+getByRole\\(\\s*(['"\`])${escapeRegExp2(role)}\\1\\s*,\\s*\\{[^}]*?name:\\s*(['"\`])${escapeRegExp2(name)}\\2[^}]*?exact:\\s*true[^}]*\\}\\s*\\)`);
|
|
303284
303826
|
if (!akaPattern.test(error2))
|
|
303285
303827
|
return null;
|
|
303286
|
-
const codePattern = new RegExp(`(getByRole\\(\\s*(['"\`])${
|
|
303828
|
+
const codePattern = new RegExp(`(getByRole\\(\\s*(['"\`])${escapeRegExp2(role)}\\2\\s*,\\s*\\{[^}]*?name:\\s*(['"\`])${escapeRegExp2(name)}\\3)([^}]*\\})`, "g");
|
|
303287
303829
|
let modified = false;
|
|
303288
303830
|
const patched = code.replace(codePattern, (full, prefix, _q1, _q2, suffix) => {
|
|
303289
303831
|
if (/exact\s*:\s*true/.test(suffix))
|
|
@@ -303300,11 +303842,11 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
|
|
|
303300
303842
|
const [, , text, optionsBlock] = violated;
|
|
303301
303843
|
if (optionsBlock && /exact\s*:\s*true/.test(optionsBlock))
|
|
303302
303844
|
return null;
|
|
303303
|
-
const akaPattern = new RegExp(`aka\\s+getByText\\(\\s*(['"\`])${
|
|
303845
|
+
const akaPattern = new RegExp(`aka\\s+getByText\\(\\s*(['"\`])${escapeRegExp2(text)}\\1\\s*,\\s*\\{[^}]*?exact:\\s*true[^}]*\\}\\s*\\)`);
|
|
303304
303846
|
if (!akaPattern.test(error2))
|
|
303305
303847
|
return null;
|
|
303306
|
-
const noOptsPattern = new RegExp(`getByText\\(\\s*(['"\`])${
|
|
303307
|
-
const withOptsPattern = new RegExp(`(getByText\\(\\s*(['"\`])${
|
|
303848
|
+
const noOptsPattern = new RegExp(`getByText\\(\\s*(['"\`])${escapeRegExp2(text)}\\1\\s*\\)`, "g");
|
|
303849
|
+
const withOptsPattern = new RegExp(`(getByText\\(\\s*(['"\`])${escapeRegExp2(text)}\\2\\s*,\\s*\\{)([^}]*\\})`, "g");
|
|
303308
303850
|
let modified = false;
|
|
303309
303851
|
let patched = code.replace(noOptsPattern, (_full, q) => {
|
|
303310
303852
|
modified = true;
|
|
@@ -308478,6 +309020,7 @@ var require_regression_runner = __commonJS({
|
|
|
308478
309020
|
var chromium_isolation_js_1 = require_chromium_isolation();
|
|
308479
309021
|
var browser_config_js_1 = require_browser_config();
|
|
308480
309022
|
var spawn_with_tree_kill_js_1 = require_spawn_with_tree_kill();
|
|
309023
|
+
var run_callback_proxy_js_1 = require_run_callback_proxy();
|
|
308481
309024
|
var SUITE_SPAWN_TIMEOUT_MS = 85 * 60 * 1e3;
|
|
308482
309025
|
var PACKAGE_DIR = (0, path_1.resolve)(__dirname, "..", "..");
|
|
308483
309026
|
var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
|
|
@@ -308492,6 +309035,7 @@ var require_regression_runner = __commonJS({
|
|
|
308492
309035
|
const tarPath = (0, path_1.join)(RUNS_DIR, `regression-${runId}.tar.gz`);
|
|
308493
309036
|
const startTime = Date.now();
|
|
308494
309037
|
let logs2 = "";
|
|
309038
|
+
let callbackProxy;
|
|
308495
309039
|
try {
|
|
308496
309040
|
await (0, promises_1.mkdir)(extractDir, { recursive: true });
|
|
308497
309041
|
const tarballUrl = `https://api.github.com/repos/${repoOwner}/${repoName}/tarball/${repoBranch}`;
|
|
@@ -308521,19 +309065,40 @@ var require_regression_runner = __commonJS({
|
|
|
308521
309065
|
await (0, promises_1.writeFile)((0, path_1.join)(extractDir, ".env"), envContent, "utf-8");
|
|
308522
309066
|
const jsonResultsPath = (0, path_1.join)(extractDir, "test-results", "results.json");
|
|
308523
309067
|
const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(extractDir);
|
|
309068
|
+
const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
|
|
309069
|
+
const callbackClaimEpoch = Number.isSafeInteger(opts.claimEpoch) ? opts.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
|
|
309070
|
+
if (opts.validateServerUrl && opts.validateRunnerAuthHeader) {
|
|
309071
|
+
try {
|
|
309072
|
+
callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
|
|
309073
|
+
runId,
|
|
309074
|
+
serverUrl: opts.validateServerUrl,
|
|
309075
|
+
upstreamAuthHeader: opts.validateRunnerAuthHeader,
|
|
309076
|
+
taskArn: process.env.VALIDATE_TASK_ARN,
|
|
309077
|
+
claimEpoch: callbackClaimEpoch,
|
|
309078
|
+
authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
|
|
309079
|
+
});
|
|
309080
|
+
} catch (error2) {
|
|
309081
|
+
logs2 += `Callback proxy unavailable: ${error2 instanceof Error ? error2.message : String(error2)}
|
|
309082
|
+
`;
|
|
309083
|
+
}
|
|
309084
|
+
}
|
|
308524
309085
|
const runEnv = {
|
|
308525
309086
|
...(0, shared_1.buildSandboxedEnv)(process.env, testVariables),
|
|
308526
309087
|
...isolation.env,
|
|
308527
309088
|
CI: "true",
|
|
308528
309089
|
PLAYWRIGHT_JSON_OUTPUT_NAME: jsonResultsPath,
|
|
308529
|
-
//
|
|
308530
|
-
//
|
|
308531
|
-
|
|
308532
|
-
|
|
308533
|
-
|
|
309090
|
+
// The suite receives a random run-scoped loopback callback, never the
|
|
309091
|
+
// global runner bearer credential.
|
|
309092
|
+
...callbackProxy ? {
|
|
309093
|
+
VALIDATE_RUN_ID: runId,
|
|
309094
|
+
...callbackClaimEpoch !== void 0 ? { VALIDATE_CLAIM_EPOCH: String(callbackClaimEpoch) } : {},
|
|
309095
|
+
VALIDATE_SERVER_URL: callbackProxy.serverUrl,
|
|
309096
|
+
VALIDATE_RUNNER_AUTH: callbackProxy.authHeader
|
|
309097
|
+
} : {},
|
|
308534
309098
|
// Owning Fargate taskArn (per-task constant; unset off-Fargate). Forwarded
|
|
308535
309099
|
// so persistCredential() can prove ownership to the /credential fence.
|
|
308536
|
-
...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {}
|
|
309100
|
+
...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {},
|
|
309101
|
+
...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
|
|
308537
309102
|
};
|
|
308538
309103
|
logs2 += `Running playwright --project=${opts.suiteProject ?? "suite"}...
|
|
308539
309104
|
`;
|
|
@@ -308562,6 +309127,8 @@ var require_regression_runner = __commonJS({
|
|
|
308562
309127
|
error: String(err)
|
|
308563
309128
|
};
|
|
308564
309129
|
} finally {
|
|
309130
|
+
await callbackProxy?.close().catch(() => {
|
|
309131
|
+
});
|
|
308565
309132
|
(0, promises_1.rm)(extractDir, { recursive: true, force: true }).catch(() => {
|
|
308566
309133
|
});
|
|
308567
309134
|
await (0, browser_config_js_1.cleanupPlaywrightTempArtifacts)().catch(() => {
|
|
@@ -321236,9 +321803,9 @@ var require_tool_ledger = __commonJS({
|
|
|
321236
321803
|
const ledgerPath = (0, node_path_1.join)(runDir, "discovery-events.jsonl");
|
|
321237
321804
|
let checkpointIndex = 0;
|
|
321238
321805
|
let headerWritten = false;
|
|
321239
|
-
const
|
|
321806
|
+
const escapeRegExp2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
321240
321807
|
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]) => ({
|
|
321241
|
-
re: new RegExp(
|
|
321808
|
+
re: new RegExp(escapeRegExp2(value), "g"),
|
|
321242
321809
|
replacement: `[REDACTED_${envName}]`
|
|
321243
321810
|
}));
|
|
321244
321811
|
const scrubCredentials = (text) => {
|
|
@@ -323398,6 +323965,7 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
|
|
|
323398
323965
|
testBudget
|
|
323399
323966
|
}, {
|
|
323400
323967
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
323968
|
+
gateModelOverride: ctx.planGateModel,
|
|
323401
323969
|
onAICall
|
|
323402
323970
|
});
|
|
323403
323971
|
logs2.push(...planResult.logs);
|
|
@@ -323976,6 +324544,7 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
|
|
|
323976
324544
|
testBudget: featureTestBudget
|
|
323977
324545
|
}, {
|
|
323978
324546
|
modelOverride: ctx.planModel ?? ctx.generateModel,
|
|
324547
|
+
gateModelOverride: ctx.planGateModel,
|
|
323979
324548
|
onAICall
|
|
323980
324549
|
});
|
|
323981
324550
|
logs2.push(...planResult.logs);
|
|
@@ -324805,6 +325374,455 @@ var require_resource_governor = __commonJS({
|
|
|
324805
325374
|
}
|
|
324806
325375
|
});
|
|
324807
325376
|
|
|
325377
|
+
// ../runner-core/dist/services/local-auth-state.js
|
|
325378
|
+
var require_local_auth_state = __commonJS({
|
|
325379
|
+
"../runner-core/dist/services/local-auth-state.js"(exports2) {
|
|
325380
|
+
"use strict";
|
|
325381
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
325382
|
+
exports2.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS = exports2.LOCAL_AUTH_STATE_LOCK_STALE_MS = exports2.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS = exports2.LOCAL_AUTH_STATE_WAIT_MS = exports2.LOCAL_AUTH_STATE_MAX_AGE_MS = void 0;
|
|
325383
|
+
exports2.shouldSkipWarmAuthSetup = shouldSkipWarmAuthSetup2;
|
|
325384
|
+
exports2.shouldSkipDisabledAutomaticAuthSetup = shouldSkipDisabledAutomaticAuthSetup2;
|
|
325385
|
+
exports2.localAuthStateFingerprint = localAuthStateFingerprint;
|
|
325386
|
+
exports2.ensurePrivateDirectory = ensurePrivateDirectory2;
|
|
325387
|
+
exports2.isValidStorageState = isValidStorageState;
|
|
325388
|
+
exports2.isValidStorageStateJson = isValidStorageStateJson;
|
|
325389
|
+
exports2.isValidLocalAuthStateFile = isValidLocalAuthStateFile;
|
|
325390
|
+
exports2.cleanupLocalAuthStateFiles = cleanupLocalAuthStateFiles;
|
|
325391
|
+
exports2.cleanupExpiredLocalAuthStateFiles = cleanupExpiredLocalAuthStateFiles2;
|
|
325392
|
+
exports2.localAuthStatePaths = localAuthStatePaths2;
|
|
325393
|
+
exports2.getValidLocalAuthState = getValidLocalAuthState2;
|
|
325394
|
+
exports2.waitForValidLocalAuthState = waitForValidLocalAuthState;
|
|
325395
|
+
exports2.waitForLocalAuthStateProducer = waitForLocalAuthStateProducer2;
|
|
325396
|
+
exports2.promoteLocalAuthStateCapture = promoteLocalAuthStateCapture2;
|
|
325397
|
+
exports2.discardLocalAuthStateCapture = discardLocalAuthStateCapture2;
|
|
325398
|
+
exports2.acquireLocalAuthStateLock = acquireLocalAuthStateLock2;
|
|
325399
|
+
exports2.localAuthStateFilename = localAuthStateFilename;
|
|
325400
|
+
var node_fs_1 = require("fs");
|
|
325401
|
+
var node_crypto_1 = require("crypto");
|
|
325402
|
+
var node_path_1 = require("path");
|
|
325403
|
+
var auth_recipe_js_1 = require_auth_recipe();
|
|
325404
|
+
exports2.LOCAL_AUTH_STATE_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
325405
|
+
exports2.LOCAL_AUTH_STATE_WAIT_MS = 3 * 60 * 1e3;
|
|
325406
|
+
exports2.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS = 15 * 60 * 1e3;
|
|
325407
|
+
exports2.LOCAL_AUTH_STATE_LOCK_STALE_MS = 2 * 60 * 1e3;
|
|
325408
|
+
exports2.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS = 30 * 60 * 1e3;
|
|
325409
|
+
function shouldSkipWarmAuthSetup2(authForceRun, sharedAuthEnabled, cacheWarm) {
|
|
325410
|
+
return sharedAuthEnabled && authForceRun !== true && cacheWarm;
|
|
325411
|
+
}
|
|
325412
|
+
function shouldSkipDisabledAutomaticAuthSetup2(authForceRun, authVerify, sharedAuthEnabled) {
|
|
325413
|
+
return !sharedAuthEnabled && authForceRun !== true && authVerify !== true;
|
|
325414
|
+
}
|
|
325415
|
+
function normalizeOrigin(value) {
|
|
325416
|
+
try {
|
|
325417
|
+
return new URL(value).origin.toLowerCase();
|
|
325418
|
+
} catch {
|
|
325419
|
+
return value.trim().toLowerCase();
|
|
325420
|
+
}
|
|
325421
|
+
}
|
|
325422
|
+
function normalizeAppUrl(value) {
|
|
325423
|
+
try {
|
|
325424
|
+
const url = new URL(value);
|
|
325425
|
+
url.hash = "";
|
|
325426
|
+
return `${url.origin.toLowerCase()}${url.pathname}${url.search}`;
|
|
325427
|
+
} catch {
|
|
325428
|
+
return value.trim();
|
|
325429
|
+
}
|
|
325430
|
+
}
|
|
325431
|
+
function safeSegment(value, fallback) {
|
|
325432
|
+
const safe = value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 120);
|
|
325433
|
+
return safe || fallback;
|
|
325434
|
+
}
|
|
325435
|
+
function stableVariables(testVariables) {
|
|
325436
|
+
return Object.entries(testVariables).filter(([key]) => !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(key)).sort(([a], [b]) => a.localeCompare(b));
|
|
325437
|
+
}
|
|
325438
|
+
function localAuthStateFingerprint(input) {
|
|
325439
|
+
const payload = JSON.stringify({
|
|
325440
|
+
version: 1,
|
|
325441
|
+
projectId: input.projectId,
|
|
325442
|
+
serverOrigin: normalizeOrigin(input.serverUrl),
|
|
325443
|
+
appUrl: normalizeAppUrl(input.baseUrl),
|
|
325444
|
+
testVariables: stableVariables(input.testVariables)
|
|
325445
|
+
});
|
|
325446
|
+
const key = input.authToken || `validate-local-auth:${input.projectId}`;
|
|
325447
|
+
return (0, node_crypto_1.createHmac)("sha256", key).update(payload).digest("hex").slice(0, 24);
|
|
325448
|
+
}
|
|
325449
|
+
function ensurePrivateDirectory2(dir) {
|
|
325450
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
325451
|
+
try {
|
|
325452
|
+
(0, node_fs_1.chmodSync)(dir, 448);
|
|
325453
|
+
} catch {
|
|
325454
|
+
}
|
|
325455
|
+
}
|
|
325456
|
+
function removeFile(path) {
|
|
325457
|
+
try {
|
|
325458
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
325459
|
+
} catch {
|
|
325460
|
+
}
|
|
325461
|
+
}
|
|
325462
|
+
function readLockOwner(path) {
|
|
325463
|
+
try {
|
|
325464
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
|
|
325465
|
+
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;
|
|
325466
|
+
} catch {
|
|
325467
|
+
return void 0;
|
|
325468
|
+
}
|
|
325469
|
+
}
|
|
325470
|
+
function processIsAlive(pid) {
|
|
325471
|
+
try {
|
|
325472
|
+
process.kill(pid, 0);
|
|
325473
|
+
return true;
|
|
325474
|
+
} catch (error2) {
|
|
325475
|
+
return error2.code === "EPERM";
|
|
325476
|
+
}
|
|
325477
|
+
}
|
|
325478
|
+
function staleLockCanBeReaped(path, now, staleMs) {
|
|
325479
|
+
try {
|
|
325480
|
+
const ageMs = now - (0, node_fs_1.statSync)(path).mtimeMs;
|
|
325481
|
+
if (ageMs < staleMs)
|
|
325482
|
+
return false;
|
|
325483
|
+
const owner = readLockOwner(path);
|
|
325484
|
+
if (owner) {
|
|
325485
|
+
if (!processIsAlive(owner.pid))
|
|
325486
|
+
return true;
|
|
325487
|
+
return ageMs >= Math.max(staleMs * 5, exports2.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS);
|
|
325488
|
+
}
|
|
325489
|
+
return ageMs >= staleMs * 5;
|
|
325490
|
+
} catch {
|
|
325491
|
+
return false;
|
|
325492
|
+
}
|
|
325493
|
+
}
|
|
325494
|
+
function isRecord(value) {
|
|
325495
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
325496
|
+
}
|
|
325497
|
+
function isValidStorageState(value) {
|
|
325498
|
+
if (!isRecord(value))
|
|
325499
|
+
return false;
|
|
325500
|
+
const cookies = value.cookies;
|
|
325501
|
+
const origins = value.origins;
|
|
325502
|
+
if (!Array.isArray(cookies) || !Array.isArray(origins))
|
|
325503
|
+
return false;
|
|
325504
|
+
const cookiesValid = cookies.every((cookie) => {
|
|
325505
|
+
if (!isRecord(cookie))
|
|
325506
|
+
return false;
|
|
325507
|
+
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");
|
|
325508
|
+
});
|
|
325509
|
+
if (!cookiesValid)
|
|
325510
|
+
return false;
|
|
325511
|
+
const originsValid = origins.every((origin) => {
|
|
325512
|
+
if (!isRecord(origin) || typeof origin.origin !== "string" || !Array.isArray(origin.localStorage))
|
|
325513
|
+
return false;
|
|
325514
|
+
return origin.localStorage.every((entry) => isRecord(entry) && typeof entry.name === "string" && typeof entry.value === "string");
|
|
325515
|
+
});
|
|
325516
|
+
if (!originsValid)
|
|
325517
|
+
return false;
|
|
325518
|
+
return cookies.length > 0 || origins.some((origin) => origin.localStorage.length > 0);
|
|
325519
|
+
}
|
|
325520
|
+
function isValidStorageStateJson(value) {
|
|
325521
|
+
try {
|
|
325522
|
+
return isValidStorageState(JSON.parse(value));
|
|
325523
|
+
} catch {
|
|
325524
|
+
return false;
|
|
325525
|
+
}
|
|
325526
|
+
}
|
|
325527
|
+
function isStorageStateShape(value) {
|
|
325528
|
+
if (!value || typeof value !== "object")
|
|
325529
|
+
return false;
|
|
325530
|
+
return isValidStorageState(value);
|
|
325531
|
+
}
|
|
325532
|
+
function isValidLocalAuthStateFile(path) {
|
|
325533
|
+
try {
|
|
325534
|
+
return isStorageStateShape(JSON.parse((0, node_fs_1.readFileSync)(path, "utf8")));
|
|
325535
|
+
} catch {
|
|
325536
|
+
return false;
|
|
325537
|
+
}
|
|
325538
|
+
}
|
|
325539
|
+
function cleanupLocalAuthStateFiles(runsDir, projectId, now = Date.now(), maxAgeMs = exports2.LOCAL_AUTH_STATE_MAX_AGE_MS) {
|
|
325540
|
+
ensurePrivateDirectory2(runsDir);
|
|
325541
|
+
const projectSegment = safeSegment(projectId, "default");
|
|
325542
|
+
const legacyName = `auth-state-${projectSegment}.json`;
|
|
325543
|
+
const prefix = `auth-state-${projectSegment}-`;
|
|
325544
|
+
for (const name of (0, node_fs_1.readdirSync)(runsDir)) {
|
|
325545
|
+
if (name === legacyName) {
|
|
325546
|
+
removeFile((0, node_path_1.join)(runsDir, name));
|
|
325547
|
+
continue;
|
|
325548
|
+
}
|
|
325549
|
+
if (!name.startsWith(prefix))
|
|
325550
|
+
continue;
|
|
325551
|
+
const path = (0, node_path_1.join)(runsDir, name);
|
|
325552
|
+
try {
|
|
325553
|
+
if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs) {
|
|
325554
|
+
if (!name.endsWith(".lock") || staleLockCanBeReaped(path, now, maxAgeMs))
|
|
325555
|
+
removeFile(path);
|
|
325556
|
+
}
|
|
325557
|
+
} catch {
|
|
325558
|
+
}
|
|
325559
|
+
}
|
|
325560
|
+
}
|
|
325561
|
+
function cleanupExpiredLocalAuthStateFiles2(runsDir, now = Date.now(), maxAgeMs = exports2.LOCAL_AUTH_STATE_MAX_AGE_MS) {
|
|
325562
|
+
ensurePrivateDirectory2(runsDir);
|
|
325563
|
+
for (const name of (0, node_fs_1.readdirSync)(runsDir)) {
|
|
325564
|
+
if (!name.startsWith("auth-state-"))
|
|
325565
|
+
continue;
|
|
325566
|
+
const path = (0, node_path_1.join)(runsDir, name);
|
|
325567
|
+
try {
|
|
325568
|
+
if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs) {
|
|
325569
|
+
if (!name.endsWith(".lock") || staleLockCanBeReaped(path, now, maxAgeMs))
|
|
325570
|
+
removeFile(path);
|
|
325571
|
+
}
|
|
325572
|
+
} catch {
|
|
325573
|
+
}
|
|
325574
|
+
}
|
|
325575
|
+
}
|
|
325576
|
+
function localAuthStatePaths2(runsDir, input, runId) {
|
|
325577
|
+
cleanupLocalAuthStateFiles(runsDir, input.projectId);
|
|
325578
|
+
const projectSegment = safeSegment(input.projectId, "default");
|
|
325579
|
+
const runSegment = safeSegment(runId, `${process.pid}`);
|
|
325580
|
+
const fingerprint = localAuthStateFingerprint(input);
|
|
325581
|
+
const cachePath = (0, node_path_1.join)(runsDir, `auth-state-${projectSegment}-${fingerprint}.json`);
|
|
325582
|
+
return {
|
|
325583
|
+
cachePath,
|
|
325584
|
+
capturePath: `${cachePath}.capture-${runSegment}-${process.pid}.tmp`,
|
|
325585
|
+
fingerprint,
|
|
325586
|
+
lockKey: `${normalizeOrigin(input.serverUrl)}:${projectSegment}:${fingerprint}`,
|
|
325587
|
+
lockPath: `${cachePath}.lock`
|
|
325588
|
+
};
|
|
325589
|
+
}
|
|
325590
|
+
function getValidLocalAuthState2(path, now = Date.now(), maxAgeMs = exports2.LOCAL_AUTH_STATE_MAX_AGE_MS) {
|
|
325591
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
325592
|
+
return void 0;
|
|
325593
|
+
try {
|
|
325594
|
+
if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs || !isValidLocalAuthStateFile(path)) {
|
|
325595
|
+
removeFile(path);
|
|
325596
|
+
return void 0;
|
|
325597
|
+
}
|
|
325598
|
+
return path;
|
|
325599
|
+
} catch {
|
|
325600
|
+
return void 0;
|
|
325601
|
+
}
|
|
325602
|
+
}
|
|
325603
|
+
async function waitForValidLocalAuthState(path, timeoutMs = exports2.LOCAL_AUTH_STATE_WAIT_MS, pollMs = 100) {
|
|
325604
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
325605
|
+
do {
|
|
325606
|
+
const valid = getValidLocalAuthState2(path);
|
|
325607
|
+
if (valid)
|
|
325608
|
+
return valid;
|
|
325609
|
+
const remaining = deadline - Date.now();
|
|
325610
|
+
if (remaining <= 0)
|
|
325611
|
+
return void 0;
|
|
325612
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.min(Math.max(10, pollMs), remaining)));
|
|
325613
|
+
} while (true);
|
|
325614
|
+
}
|
|
325615
|
+
async function waitForLocalAuthStateProducer2(path, lockKey, startupGraceMs = 2e3, maxWaitMs = exports2.LOCAL_AUTH_STATE_WAIT_MS, pollMs = 100, lockPath) {
|
|
325616
|
+
const startedAt = Date.now();
|
|
325617
|
+
const producerIsActive = () => lockTails.has(lockKey) || !!lockPath && (0, node_fs_1.existsSync)(lockPath);
|
|
325618
|
+
let sawProducer = producerIsActive();
|
|
325619
|
+
do {
|
|
325620
|
+
const valid = getValidLocalAuthState2(path);
|
|
325621
|
+
if (valid)
|
|
325622
|
+
return valid;
|
|
325623
|
+
const producerActive = producerIsActive();
|
|
325624
|
+
if (producerActive)
|
|
325625
|
+
sawProducer = true;
|
|
325626
|
+
const elapsed = Date.now() - startedAt;
|
|
325627
|
+
if (sawProducer && !producerActive)
|
|
325628
|
+
return void 0;
|
|
325629
|
+
if (!sawProducer && elapsed >= Math.max(0, startupGraceMs))
|
|
325630
|
+
return void 0;
|
|
325631
|
+
if (elapsed >= Math.max(0, maxWaitMs))
|
|
325632
|
+
return void 0;
|
|
325633
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.max(10, pollMs)));
|
|
325634
|
+
} while (true);
|
|
325635
|
+
}
|
|
325636
|
+
function promoteLocalAuthStateCapture2(capturePath, cachePath, replaceExisting = false, lease) {
|
|
325637
|
+
if (lease && !lease.isOwner()) {
|
|
325638
|
+
removeFile(capturePath);
|
|
325639
|
+
return false;
|
|
325640
|
+
}
|
|
325641
|
+
if (!(0, node_fs_1.existsSync)(capturePath) || !isValidLocalAuthStateFile(capturePath)) {
|
|
325642
|
+
removeFile(capturePath);
|
|
325643
|
+
return false;
|
|
325644
|
+
}
|
|
325645
|
+
try {
|
|
325646
|
+
(0, node_fs_1.chmodSync)(capturePath, 384);
|
|
325647
|
+
} catch {
|
|
325648
|
+
}
|
|
325649
|
+
try {
|
|
325650
|
+
if (lease && !lease.isOwner()) {
|
|
325651
|
+
removeFile(capturePath);
|
|
325652
|
+
return false;
|
|
325653
|
+
}
|
|
325654
|
+
if ((0, node_fs_1.existsSync)(cachePath)) {
|
|
325655
|
+
if (!replaceExisting && isValidLocalAuthStateFile(cachePath)) {
|
|
325656
|
+
removeFile(capturePath);
|
|
325657
|
+
return true;
|
|
325658
|
+
}
|
|
325659
|
+
removeFile(cachePath);
|
|
325660
|
+
}
|
|
325661
|
+
if (lease && !lease.isOwner()) {
|
|
325662
|
+
removeFile(capturePath);
|
|
325663
|
+
return false;
|
|
325664
|
+
}
|
|
325665
|
+
(0, node_fs_1.renameSync)(capturePath, cachePath);
|
|
325666
|
+
try {
|
|
325667
|
+
(0, node_fs_1.chmodSync)(cachePath, 384);
|
|
325668
|
+
} catch {
|
|
325669
|
+
}
|
|
325670
|
+
return true;
|
|
325671
|
+
} catch {
|
|
325672
|
+
const winnerIsValid = (0, node_fs_1.existsSync)(cachePath) && isValidLocalAuthStateFile(cachePath);
|
|
325673
|
+
removeFile(capturePath);
|
|
325674
|
+
return winnerIsValid;
|
|
325675
|
+
}
|
|
325676
|
+
}
|
|
325677
|
+
function discardLocalAuthStateCapture2(capturePath) {
|
|
325678
|
+
removeFile(capturePath);
|
|
325679
|
+
}
|
|
325680
|
+
var lockTails = /* @__PURE__ */ new Map();
|
|
325681
|
+
function publishCompleteLockOwner(lockPath, owner, linkFile) {
|
|
325682
|
+
const metadata = JSON.stringify(owner);
|
|
325683
|
+
const tempPath = `${lockPath}.${owner.pid}.${owner.token}.tmp`;
|
|
325684
|
+
let createdLock = false;
|
|
325685
|
+
try {
|
|
325686
|
+
(0, node_fs_1.writeFileSync)(tempPath, metadata, { flag: "wx", mode: 384 });
|
|
325687
|
+
try {
|
|
325688
|
+
linkFile(tempPath, lockPath);
|
|
325689
|
+
createdLock = true;
|
|
325690
|
+
return (0, node_fs_1.openSync)(lockPath, "r+");
|
|
325691
|
+
} catch (error2) {
|
|
325692
|
+
const code = error2.code;
|
|
325693
|
+
if (!["EPERM", "EACCES", "EXDEV", "ENOSYS", "ENOTSUP", "EOPNOTSUPP"].includes(code ?? "")) {
|
|
325694
|
+
throw error2;
|
|
325695
|
+
}
|
|
325696
|
+
let fd;
|
|
325697
|
+
try {
|
|
325698
|
+
fd = (0, node_fs_1.openSync)(lockPath, "wx+", 384);
|
|
325699
|
+
createdLock = true;
|
|
325700
|
+
(0, node_fs_1.writeFileSync)(fd, metadata, { encoding: "utf8" });
|
|
325701
|
+
(0, node_fs_1.fsyncSync)(fd);
|
|
325702
|
+
return fd;
|
|
325703
|
+
} catch (fallbackError) {
|
|
325704
|
+
if (fd !== void 0) {
|
|
325705
|
+
try {
|
|
325706
|
+
(0, node_fs_1.closeSync)(fd);
|
|
325707
|
+
} catch {
|
|
325708
|
+
}
|
|
325709
|
+
}
|
|
325710
|
+
if (createdLock)
|
|
325711
|
+
removeFile(lockPath);
|
|
325712
|
+
throw fallbackError;
|
|
325713
|
+
}
|
|
325714
|
+
}
|
|
325715
|
+
} catch (error2) {
|
|
325716
|
+
if (createdLock && readLockOwner(lockPath)?.token === owner.token)
|
|
325717
|
+
removeFile(lockPath);
|
|
325718
|
+
throw error2;
|
|
325719
|
+
} finally {
|
|
325720
|
+
removeFile(tempPath);
|
|
325721
|
+
}
|
|
325722
|
+
}
|
|
325723
|
+
async function acquireExclusiveFileLock(lockPath, linkFile = node_fs_1.linkSync) {
|
|
325724
|
+
ensurePrivateDirectory2((0, node_path_1.dirname)(lockPath));
|
|
325725
|
+
const startedAt = Date.now();
|
|
325726
|
+
while (true) {
|
|
325727
|
+
try {
|
|
325728
|
+
const ownerToken = (0, node_crypto_1.randomBytes)(24).toString("base64url");
|
|
325729
|
+
const fd = publishCompleteLockOwner(lockPath, { token: ownerToken, pid: process.pid }, linkFile);
|
|
325730
|
+
const ownsPath = () => {
|
|
325731
|
+
try {
|
|
325732
|
+
const owner = readLockOwner(lockPath);
|
|
325733
|
+
return owner?.token === ownerToken && owner.pid === process.pid;
|
|
325734
|
+
} catch {
|
|
325735
|
+
return false;
|
|
325736
|
+
}
|
|
325737
|
+
};
|
|
325738
|
+
const heartbeat = setInterval(() => {
|
|
325739
|
+
try {
|
|
325740
|
+
if (!ownsPath()) {
|
|
325741
|
+
clearInterval(heartbeat);
|
|
325742
|
+
return;
|
|
325743
|
+
}
|
|
325744
|
+
const now = /* @__PURE__ */ new Date();
|
|
325745
|
+
(0, node_fs_1.futimesSync)(fd, now, now);
|
|
325746
|
+
} catch {
|
|
325747
|
+
clearInterval(heartbeat);
|
|
325748
|
+
}
|
|
325749
|
+
}, Math.max(1e3, Math.floor(exports2.LOCAL_AUTH_STATE_LOCK_STALE_MS / 3)));
|
|
325750
|
+
heartbeat.unref();
|
|
325751
|
+
let released = false;
|
|
325752
|
+
const release = (() => {
|
|
325753
|
+
if (released)
|
|
325754
|
+
return;
|
|
325755
|
+
released = true;
|
|
325756
|
+
clearInterval(heartbeat);
|
|
325757
|
+
try {
|
|
325758
|
+
if (ownsPath())
|
|
325759
|
+
removeFile(lockPath);
|
|
325760
|
+
} catch {
|
|
325761
|
+
}
|
|
325762
|
+
try {
|
|
325763
|
+
(0, node_fs_1.closeSync)(fd);
|
|
325764
|
+
} catch {
|
|
325765
|
+
}
|
|
325766
|
+
});
|
|
325767
|
+
release.isOwner = () => !released && ownsPath();
|
|
325768
|
+
return release;
|
|
325769
|
+
} catch (error2) {
|
|
325770
|
+
const code = error2.code;
|
|
325771
|
+
if (code !== "EEXIST")
|
|
325772
|
+
throw error2;
|
|
325773
|
+
try {
|
|
325774
|
+
if (staleLockCanBeReaped(lockPath, Date.now(), exports2.LOCAL_AUTH_STATE_LOCK_STALE_MS)) {
|
|
325775
|
+
removeFile(lockPath);
|
|
325776
|
+
continue;
|
|
325777
|
+
}
|
|
325778
|
+
} catch {
|
|
325779
|
+
continue;
|
|
325780
|
+
}
|
|
325781
|
+
if (Date.now() - startedAt >= exports2.LOCAL_AUTH_STATE_WAIT_MS) {
|
|
325782
|
+
throw new Error(`Timed out waiting for auth-state lock: ${(0, node_path_1.basename)(lockPath)}`);
|
|
325783
|
+
}
|
|
325784
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
325785
|
+
}
|
|
325786
|
+
}
|
|
325787
|
+
}
|
|
325788
|
+
async function acquireLocalAuthStateLock2(key, lockPath, linkFile = node_fs_1.linkSync) {
|
|
325789
|
+
const previous = lockTails.get(key) ?? Promise.resolve();
|
|
325790
|
+
let unlockCurrent;
|
|
325791
|
+
const current = new Promise((resolve2) => {
|
|
325792
|
+
unlockCurrent = resolve2;
|
|
325793
|
+
});
|
|
325794
|
+
const tail = previous.then(() => current);
|
|
325795
|
+
lockTails.set(key, tail);
|
|
325796
|
+
await previous;
|
|
325797
|
+
let releaseFileLock;
|
|
325798
|
+
try {
|
|
325799
|
+
if (lockPath)
|
|
325800
|
+
releaseFileLock = await acquireExclusiveFileLock(lockPath, linkFile);
|
|
325801
|
+
} catch (error2) {
|
|
325802
|
+
unlockCurrent();
|
|
325803
|
+
if (lockTails.get(key) === tail)
|
|
325804
|
+
lockTails.delete(key);
|
|
325805
|
+
throw error2;
|
|
325806
|
+
}
|
|
325807
|
+
let released = false;
|
|
325808
|
+
const release = (() => {
|
|
325809
|
+
if (released)
|
|
325810
|
+
return;
|
|
325811
|
+
released = true;
|
|
325812
|
+
releaseFileLock?.();
|
|
325813
|
+
unlockCurrent();
|
|
325814
|
+
if (lockTails.get(key) === tail)
|
|
325815
|
+
lockTails.delete(key);
|
|
325816
|
+
});
|
|
325817
|
+
release.isOwner = () => !released && (releaseFileLock?.isOwner() ?? true);
|
|
325818
|
+
return release;
|
|
325819
|
+
}
|
|
325820
|
+
function localAuthStateFilename(path) {
|
|
325821
|
+
return (0, node_path_1.basename)(path);
|
|
325822
|
+
}
|
|
325823
|
+
}
|
|
325824
|
+
});
|
|
325825
|
+
|
|
324808
325826
|
// ../../node_modules/adm-zip/util/constants.js
|
|
324809
325827
|
var require_constants5 = __commonJS({
|
|
324810
325828
|
"../../node_modules/adm-zip/util/constants.js"(exports2, module2) {
|
|
@@ -325118,7 +326136,7 @@ var require_utils2 = __commonJS({
|
|
|
325118
326136
|
module2.exports = Utils;
|
|
325119
326137
|
Utils.prototype.makeDir = function(folder) {
|
|
325120
326138
|
const self = this;
|
|
325121
|
-
function
|
|
326139
|
+
function mkdirSync4(fpath) {
|
|
325122
326140
|
let resolvedPath = fpath.split(self.sep)[0];
|
|
325123
326141
|
fpath.split(self.sep).forEach(function(name) {
|
|
325124
326142
|
if (!name || name.substr(-1, 1) === ":") return;
|
|
@@ -325136,7 +326154,7 @@ var require_utils2 = __commonJS({
|
|
|
325136
326154
|
if (stat && stat.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
|
|
325137
326155
|
});
|
|
325138
326156
|
}
|
|
325139
|
-
|
|
326157
|
+
mkdirSync4(folder);
|
|
325140
326158
|
};
|
|
325141
326159
|
Utils.prototype.writeFileTo = function(path, content, overwrite, attr) {
|
|
325142
326160
|
const self = this;
|
|
@@ -337060,6 +338078,8 @@ var require_dist2 = __commonJS({
|
|
|
337060
338078
|
__exportStar(require_discover_agent(), exports2);
|
|
337061
338079
|
__exportStar(require_browser_pool(), exports2);
|
|
337062
338080
|
__exportStar(require_resource_governor(), exports2);
|
|
338081
|
+
__exportStar(require_local_auth_state(), exports2);
|
|
338082
|
+
__exportStar(require_run_callback_proxy(), exports2);
|
|
337063
338083
|
__exportStar(require_discover_explorer(), exports2);
|
|
337064
338084
|
__exportStar(require_discover_feature_explorer(), exports2);
|
|
337065
338085
|
__exportStar(require_discover_planner(), exports2);
|
|
@@ -337336,7 +338356,7 @@ var require_package4 = __commonJS({
|
|
|
337336
338356
|
"package.json"(exports2, module2) {
|
|
337337
338357
|
module2.exports = {
|
|
337338
338358
|
name: "@validate.qa/runner",
|
|
337339
|
-
version: "1.0.
|
|
338359
|
+
version: "1.0.44",
|
|
337340
338360
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
337341
338361
|
bin: {
|
|
337342
338362
|
"validate-runner": "dist/cli.js"
|
|
@@ -337344,7 +338364,7 @@ var require_package4 = __commonJS({
|
|
|
337344
338364
|
scripts: {
|
|
337345
338365
|
build: "tsup",
|
|
337346
338366
|
dev: "tsx src/cli.ts",
|
|
337347
|
-
test: "node --import tsx --test src/version-check.test.ts",
|
|
338367
|
+
test: "node --import tsx --test src/version-check.test.ts src/services/auth-state.test.ts",
|
|
337348
338368
|
"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",
|
|
337349
338369
|
prepublishOnly: "npm run build"
|
|
337350
338370
|
},
|
|
@@ -337423,7 +338443,6 @@ function getConfigDir() {
|
|
|
337423
338443
|
}
|
|
337424
338444
|
|
|
337425
338445
|
// src/runner.ts
|
|
337426
|
-
var import_fs5 = require("fs");
|
|
337427
338446
|
var import_path4 = require("path");
|
|
337428
338447
|
var import_os6 = require("os");
|
|
337429
338448
|
var import_node_crypto2 = require("crypto");
|
|
@@ -337451,26 +338470,28 @@ var import_runner_core6 = __toESM(require_dist2());
|
|
|
337451
338470
|
|
|
337452
338471
|
// src/runner.ts
|
|
337453
338472
|
var import_runner_core11 = __toESM(require_dist2());
|
|
338473
|
+
var import_runner_core12 = __toESM(require_dist2());
|
|
337454
338474
|
|
|
337455
338475
|
// src/services/auth-state.ts
|
|
337456
|
-
|
|
338476
|
+
init_dist();
|
|
337457
338477
|
var GUARDED_LOGIN_TAG = "@auth-guarded";
|
|
337458
338478
|
function sharedAuthInjectEnabled() {
|
|
337459
|
-
return
|
|
338479
|
+
return isSharedAuthInjectEnabled();
|
|
337460
338480
|
}
|
|
337461
|
-
function
|
|
338481
|
+
function hasGeneratedAuthGuard2(playwrightCode) {
|
|
337462
338482
|
if (!playwrightCode) return false;
|
|
337463
|
-
return
|
|
338483
|
+
return /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
|
|
338484
|
+
}
|
|
338485
|
+
function shouldCaptureAuthState(tags, playwrightCode) {
|
|
338486
|
+
return sharedAuthInjectEnabled() && Array.isArray(tags) && tags.includes("@needs-auth") && tags.includes(GUARDED_LOGIN_TAG) && !tags.includes("@auth") && hasGeneratedAuthGuard2(playwrightCode);
|
|
337464
338487
|
}
|
|
337465
338488
|
function shouldUseAuthState(tags, playwrightCode) {
|
|
338489
|
+
if (!sharedAuthInjectEnabled()) return false;
|
|
337466
338490
|
if (!Array.isArray(tags)) return false;
|
|
337467
|
-
|
|
337468
|
-
|
|
337469
|
-
|
|
337470
|
-
|
|
337471
|
-
const isGeneratedVariant = tags.includes("@edge") || tags.includes("@bug");
|
|
337472
|
-
const hasLegacyInlineLogin = tags.includes("@discovered") && !isGeneratedVariant && hasInlineLoginSteps(playwrightCode);
|
|
337473
|
-
return !tags.includes(INLINE_LOGIN_TAG) && !hasLegacyInlineLogin;
|
|
338491
|
+
return authPlanFor(tags, playwrightCode).needsCookieState;
|
|
338492
|
+
}
|
|
338493
|
+
function authPlanFor(tags, playwrightCode) {
|
|
338494
|
+
return deriveAuthPlan(tags, playwrightCode, { sharedAuthInjectEnabled: sharedAuthInjectEnabled() });
|
|
337474
338495
|
}
|
|
337475
338496
|
|
|
337476
338497
|
// src/version-check.ts
|
|
@@ -344177,8 +345198,8 @@ function withMediaReferences(result, refs) {
|
|
|
344177
345198
|
|
|
344178
345199
|
// src/runner.ts
|
|
344179
345200
|
init_dist();
|
|
344180
|
-
var import_runner_core12 = __toESM(require_dist2());
|
|
344181
345201
|
var import_runner_core13 = __toESM(require_dist2());
|
|
345202
|
+
var import_runner_core14 = __toESM(require_dist2());
|
|
344182
345203
|
function fingerprintTest(test) {
|
|
344183
345204
|
const code = test.framework === "APPIUM" ? [test.appiumCode ?? "", JSON.stringify(test.steps ?? [])].join("\n--steps--\n") : test.playwrightCode ?? "";
|
|
344184
345205
|
return (0, import_node_crypto2.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);
|
|
@@ -344302,21 +345323,11 @@ var log = {
|
|
|
344302
345323
|
warn: (msg) => console.log(`${C2.yellow}\u26A0${C2.reset} ${msg}`),
|
|
344303
345324
|
dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
|
|
344304
345325
|
};
|
|
344305
|
-
var AUTH_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
344306
345326
|
function getRunsDir() {
|
|
344307
345327
|
const dir = process.env.RUNS_DIR || (0, import_path4.join)((0, import_os6.homedir)(), ".validate.qa", "runs");
|
|
344308
|
-
|
|
345328
|
+
(0, import_runner_core12.ensurePrivateDirectory)(dir);
|
|
344309
345329
|
return dir;
|
|
344310
345330
|
}
|
|
344311
|
-
function authStatePath(projectId) {
|
|
344312
|
-
return (0, import_path4.join)(getRunsDir(), `auth-state-${projectId || "default"}.json`);
|
|
344313
|
-
}
|
|
344314
|
-
function getValidAuthState(projectId) {
|
|
344315
|
-
const path = authStatePath(projectId);
|
|
344316
|
-
if (!(0, import_fs5.existsSync)(path)) return void 0;
|
|
344317
|
-
const age = Date.now() - (0, import_fs5.statSync)(path).mtimeMs;
|
|
344318
|
-
return age < AUTH_MAX_AGE_MS ? path : void 0;
|
|
344319
|
-
}
|
|
344320
345331
|
function buildHeaders(authToken, projectId) {
|
|
344321
345332
|
return {
|
|
344322
345333
|
"Content-Type": "application/json",
|
|
@@ -344365,7 +345376,13 @@ async function claimRun(serverUrl, headers, runId) {
|
|
|
344365
345376
|
if (res.status === 429) return false;
|
|
344366
345377
|
if (!res.ok) throw new Error(`Claim failed: ${res.status}`);
|
|
344367
345378
|
const body = await res.json();
|
|
344368
|
-
|
|
345379
|
+
if (!body.claimed) {
|
|
345380
|
+
throw new Error("Claim response did not confirm ownership");
|
|
345381
|
+
}
|
|
345382
|
+
if (typeof body.claimEpoch !== "number" || !Number.isSafeInteger(body.claimEpoch) || body.claimEpoch < 0) {
|
|
345383
|
+
throw new Error("Claim response did not include a valid ownership epoch");
|
|
345384
|
+
}
|
|
345385
|
+
return body.claimEpoch;
|
|
344369
345386
|
}
|
|
344370
345387
|
async function postResult(serverUrl, headers, runId, result) {
|
|
344371
345388
|
const MAX_ATTEMPTS = 3;
|
|
@@ -344533,12 +345550,12 @@ async function postCaptureResult(serverUrl, headers, runId, result) {
|
|
|
344533
345550
|
});
|
|
344534
345551
|
if (!res.ok) throw new Error(`Failed to post capture result: ${res.status}`);
|
|
344535
345552
|
}
|
|
344536
|
-
async function postCredentialRotation(serverUrl, headers, runId, body) {
|
|
345553
|
+
async function postCredentialRotation(serverUrl, headers, runId, body, claimEpoch) {
|
|
344537
345554
|
try {
|
|
344538
345555
|
const res = await fetch(`${serverUrl}/api/runner/runs/${runId}/credential`, {
|
|
344539
345556
|
method: "POST",
|
|
344540
345557
|
headers,
|
|
344541
|
-
body: JSON.stringify(body),
|
|
345558
|
+
body: JSON.stringify({ ...body, ...claimEpoch !== void 0 ? { claimEpoch } : {} }),
|
|
344542
345559
|
signal: AbortSignal.timeout(3e4)
|
|
344543
345560
|
});
|
|
344544
345561
|
if (!res.ok) {
|
|
@@ -344870,11 +345887,28 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
344870
345887
|
requestLiveBrowserHeartbeat();
|
|
344871
345888
|
}, 5e3);
|
|
344872
345889
|
liveBrowserHeartbeatInterval.unref?.();
|
|
345890
|
+
const authPlan = authPlanFor(run2.tags, run2.playwrightCode);
|
|
345891
|
+
const runAuthCachePath = (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
|
|
345892
|
+
projectId: run2.projectId || opts.projectId || "default",
|
|
345893
|
+
serverUrl: opts.serverUrl,
|
|
345894
|
+
baseUrl: run2.baseUrl,
|
|
345895
|
+
testVariables: run2.testVariables,
|
|
345896
|
+
authToken: opts.authToken
|
|
345897
|
+
}, run2.runId).cachePath;
|
|
345898
|
+
const wrapperStorageStatePath = authPlan.needsCookieState && runMode !== "auth-setup" ? (0, import_runner_core12.getValidLocalAuthState)(runAuthCachePath) : void 0;
|
|
344873
345899
|
const runNativeWithLiveBrowser = (nativeOptions) => (0, import_runner_core.executeTestViaNativePlaywright)({
|
|
345900
|
+
authStandingMode: authPlan.standingHeadersMode,
|
|
345901
|
+
storageStatePath: wrapperStorageStatePath,
|
|
344874
345902
|
...nativeOptions,
|
|
344875
345903
|
liveBrowserHandle,
|
|
344876
345904
|
onLiveBrowserSnapshot: requestLiveBrowserHeartbeat
|
|
344877
345905
|
});
|
|
345906
|
+
const callbackContext = {
|
|
345907
|
+
runId: run2.runId,
|
|
345908
|
+
serverUrl: opts.serverUrl,
|
|
345909
|
+
authHeader: headers.Authorization,
|
|
345910
|
+
claimEpoch: run2.claimEpoch
|
|
345911
|
+
};
|
|
344878
345912
|
const startTime = Date.now();
|
|
344879
345913
|
try {
|
|
344880
345914
|
if (mobileLeaseError) {
|
|
@@ -344892,11 +345926,24 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
344892
345926
|
if (!run2.baseUrl) {
|
|
344893
345927
|
preflightError = "Pre-flight failed: Missing BASE_URL. Configure it in Test Data \u2192 Test Variables.";
|
|
344894
345928
|
} else {
|
|
344895
|
-
const
|
|
344896
|
-
if (
|
|
344897
|
-
preflightError =
|
|
344898
|
-
} else if (
|
|
344899
|
-
preflightError = "Pre-flight failed:
|
|
345929
|
+
const validStanding = (0, import_runner_core13.parseApiAuthHeaders)(run2.testVariables?.["API_AUTH_HEADERS"]) !== void 0;
|
|
345930
|
+
if (authPlan.configurationError) {
|
|
345931
|
+
preflightError = `Pre-flight failed: ${authPlan.configurationError}. Fix the test's auth-type tag before running.`;
|
|
345932
|
+
} else if (authPlan.needsStandingHeaders && !validStanding) {
|
|
345933
|
+
preflightError = "Pre-flight failed: This @auth-standing @api test requires a valid API_AUTH_HEADERS JSON object (header name \u2192 value).";
|
|
345934
|
+
} else {
|
|
345935
|
+
const legacyHeaderBypass = authPlan.source === "legacy" && authPlan.standingHeadersMode === "legacy-if-configured" && validStanding;
|
|
345936
|
+
const code = run2.playwrightCode ?? "";
|
|
345937
|
+
const codeProvided = code.trim().length > 0;
|
|
345938
|
+
const codeUsesCreds = codeProvided && /TEST_USER_EMAIL|TEST_USER_PASSWORD|__vqaLogin/.test(code);
|
|
345939
|
+
const tags = Array.isArray(run2.tags) ? run2.tags : [];
|
|
345940
|
+
const isHallucinationShape = codeProvided && !codeUsesCreds && tags.includes("@inline-login") && !tags.includes("@api");
|
|
345941
|
+
const requireCreds = authPlan.needsLoginCreds && !legacyHeaderBypass && !isHallucinationShape;
|
|
345942
|
+
if (requireCreds && !run2.testVariables?.["TEST_USER_EMAIL"]?.trim()) {
|
|
345943
|
+
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.";
|
|
345944
|
+
} else if (requireCreds && !run2.testVariables?.["TEST_USER_PASSWORD"]?.trim()) {
|
|
345945
|
+
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.";
|
|
345946
|
+
}
|
|
344900
345947
|
}
|
|
344901
345948
|
}
|
|
344902
345949
|
if (preflightError) {
|
|
@@ -344904,7 +345951,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
344904
345951
|
log.fail(`PRE-FLIGHT FAILED - ${preflightError}`);
|
|
344905
345952
|
return;
|
|
344906
345953
|
}
|
|
344907
|
-
const preflight = await (0,
|
|
345954
|
+
const preflight = await (0, import_runner_core13.resolveBaseUrlPreflight)(run2.baseUrl);
|
|
344908
345955
|
if (preflight.error) {
|
|
344909
345956
|
await postResult(opts.serverUrl, headers, run2.runId, { status: "ERROR", error: preflight.error });
|
|
344910
345957
|
log.fail(`PRE-FLIGHT FAILED - ${preflight.error}`);
|
|
@@ -344921,31 +345968,72 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
344921
345968
|
log.fail("ERROR - No auth setup code");
|
|
344922
345969
|
return;
|
|
344923
345970
|
}
|
|
344924
|
-
const
|
|
344925
|
-
|
|
344926
|
-
|
|
344927
|
-
|
|
345971
|
+
const sharedAuthEnabled = sharedAuthInjectEnabled();
|
|
345972
|
+
if ((0, import_runner_core12.shouldSkipDisabledAutomaticAuthSetup)(run2.authForceRun, run2.authVerify, sharedAuthEnabled)) {
|
|
345973
|
+
const duration2 = Date.now() - startTime;
|
|
345974
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
345975
|
+
status: "PASSED",
|
|
345976
|
+
logs: "Automatic auth prewarm skipped: shared session reuse is disabled.",
|
|
345977
|
+
duration: duration2
|
|
345978
|
+
});
|
|
345979
|
+
log.info(`Auth prewarm skipped (${duration2}ms) - SHARED_AUTH_INJECT=false`);
|
|
345980
|
+
return;
|
|
345981
|
+
}
|
|
345982
|
+
const authPaths2 = sharedAuthEnabled ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
|
|
345983
|
+
projectId: run2.projectId || opts.projectId || "default",
|
|
345984
|
+
serverUrl: opts.serverUrl,
|
|
344928
345985
|
baseUrl: run2.baseUrl,
|
|
344929
|
-
|
|
344930
|
-
|
|
344931
|
-
|
|
344932
|
-
|
|
344933
|
-
|
|
344934
|
-
|
|
345986
|
+
testVariables: run2.testVariables,
|
|
345987
|
+
authToken: opts.authToken
|
|
345988
|
+
}, run2.runId) : void 0;
|
|
345989
|
+
const releaseAuthLock2 = authPaths2 ? await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths2.lockKey, authPaths2.lockPath) : void 0;
|
|
345990
|
+
try {
|
|
345991
|
+
if (authPaths2 && (0, import_runner_core12.shouldSkipWarmAuthSetup)(
|
|
345992
|
+
run2.authForceRun,
|
|
345993
|
+
sharedAuthEnabled,
|
|
345994
|
+
!!(0, import_runner_core12.getValidLocalAuthState)(authPaths2.cachePath)
|
|
345995
|
+
)) {
|
|
345996
|
+
const duration3 = Date.now() - startTime;
|
|
345997
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
345998
|
+
status: "PASSED",
|
|
345999
|
+
logs: "Auth setup skipped: the credential-bound local session is already warm.",
|
|
346000
|
+
duration: duration3
|
|
346001
|
+
});
|
|
346002
|
+
log.ok(`Auth setup skipped (${duration3}ms) - local session already warm`);
|
|
346003
|
+
return;
|
|
344935
346004
|
}
|
|
344936
|
-
|
|
344937
|
-
|
|
344938
|
-
|
|
344939
|
-
|
|
344940
|
-
|
|
344941
|
-
|
|
344942
|
-
|
|
344943
|
-
|
|
344944
|
-
|
|
344945
|
-
|
|
344946
|
-
|
|
344947
|
-
|
|
344948
|
-
|
|
346005
|
+
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");
|
|
346006
|
+
const capturePath = sharedAuthEnabled && !run2.authVerify ? authPaths2?.capturePath : void 0;
|
|
346007
|
+
const result2 = await runNativeWithLiveBrowser({
|
|
346008
|
+
playwrightCode: run2.playwrightCode,
|
|
346009
|
+
baseUrl: run2.baseUrl,
|
|
346010
|
+
testName: "auth-setup",
|
|
346011
|
+
testVariables: run2.testVariables,
|
|
346012
|
+
authStateCapturePath: capturePath,
|
|
346013
|
+
validateContext: callbackContext
|
|
346014
|
+
});
|
|
346015
|
+
const captured = !!capturePath && !!authPaths2 && result2.passed && !result2.suspiciousPass && (0, import_runner_core12.promoteLocalAuthStateCapture)(
|
|
346016
|
+
capturePath,
|
|
346017
|
+
authPaths2.cachePath,
|
|
346018
|
+
run2.authForceRun === true,
|
|
346019
|
+
releaseAuthLock2
|
|
346020
|
+
);
|
|
346021
|
+
const duration2 = Date.now() - startTime;
|
|
346022
|
+
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
346023
|
+
status: result2.passed ? "PASSED" : "FAILED",
|
|
346024
|
+
apiCalls: result2.apiCalls,
|
|
346025
|
+
logs: result2.logs,
|
|
346026
|
+
error: result2.error,
|
|
346027
|
+
duration: duration2
|
|
346028
|
+
});
|
|
346029
|
+
log[result2.passed ? "ok" : "fail"](
|
|
346030
|
+
`Auth setup ${result2.passed ? "PASSED" : "FAILED"} (${duration2}ms)${captured ? " - session cached" : ""}${result2.error ? ` - ${result2.error}` : ""}`
|
|
346031
|
+
);
|
|
346032
|
+
return;
|
|
346033
|
+
} finally {
|
|
346034
|
+
if (authPaths2?.capturePath) (0, import_runner_core12.discardLocalAuthStateCapture)(authPaths2.capturePath);
|
|
346035
|
+
releaseAuthLock2?.();
|
|
346036
|
+
}
|
|
344949
346037
|
}
|
|
344950
346038
|
if (runMode === "batch-heal") {
|
|
344951
346039
|
const tests = run2.batchHealTests ?? [];
|
|
@@ -344964,7 +346052,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
344964
346052
|
const batchResult = await (0, import_runner_core10.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
|
|
344965
346053
|
testVariables: run2.testVariables,
|
|
344966
346054
|
runNativeTest: runNativeWithLiveBrowser,
|
|
344967
|
-
validateContext:
|
|
346055
|
+
validateContext: callbackContext,
|
|
344968
346056
|
onLog: healLogStream.onLog
|
|
344969
346057
|
});
|
|
344970
346058
|
await healLogStream.flushNow();
|
|
@@ -345000,6 +346088,15 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
345000
346088
|
);
|
|
345001
346089
|
const describeKind = isSerialDescribe ? "serial" : "non-serial";
|
|
345002
346090
|
log.info(`API test heal - ${subTestCount} sub-test(s) [${describeKind}], ${failingSubTestCount} budgeted, ${MAX_API_HEAL_ATTEMPTS} attempts`);
|
|
346091
|
+
const apiHealAuthPaths = shouldUseAuthState(run2.tags, run2.playwrightCode) ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
|
|
346092
|
+
projectId: run2.projectId || opts.projectId,
|
|
346093
|
+
serverUrl: opts.serverUrl,
|
|
346094
|
+
baseUrl: run2.baseUrl,
|
|
346095
|
+
testVariables: run2.testVariables,
|
|
346096
|
+
authToken: opts.authToken
|
|
346097
|
+
}, run2.runId) : void 0;
|
|
346098
|
+
const apiHealStorageStatePath = apiHealAuthPaths ? (0, import_runner_core12.getValidLocalAuthState)(apiHealAuthPaths.cachePath) : void 0;
|
|
346099
|
+
if (apiHealStorageStatePath) log.info("API heal verification is using saved auth state");
|
|
345003
346100
|
const stickyAppBugs = /* @__PURE__ */ new Map();
|
|
345004
346101
|
const lastChangeByTitle = /* @__PURE__ */ new Map();
|
|
345005
346102
|
let currentError = run2.lastFailureError ?? "Test failed";
|
|
@@ -345065,7 +346162,8 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
345065
346162
|
playwrightCode: lastHealedCode,
|
|
345066
346163
|
baseUrl: run2.baseUrl,
|
|
345067
346164
|
testVariables: run2.testVariables,
|
|
345068
|
-
|
|
346165
|
+
storageStatePath: apiHealStorageStatePath,
|
|
346166
|
+
validateContext: callbackContext
|
|
345069
346167
|
});
|
|
345070
346168
|
if (finalVerifyResult.passed) {
|
|
345071
346169
|
log.ok(` Attempt ${attempt} terminal verification PASSED (${bugCount} app_bug, ${passCount} pass)`);
|
|
@@ -345085,7 +346183,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345085
346183
|
finalVerifyResult = await runNativeWithLiveBrowser({
|
|
345086
346184
|
playwrightCode: lastHealedCode,
|
|
345087
346185
|
baseUrl: run2.baseUrl,
|
|
345088
|
-
testVariables: run2.testVariables
|
|
346186
|
+
testVariables: run2.testVariables,
|
|
346187
|
+
storageStatePath: apiHealStorageStatePath,
|
|
346188
|
+
validateContext: callbackContext
|
|
345089
346189
|
});
|
|
345090
346190
|
const stepResults = finalVerifyResult.stepResults ?? [];
|
|
345091
346191
|
const nonAppBugFails = stepResults.filter(
|
|
@@ -345135,7 +346235,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345135
346235
|
playwrightCode: lastHealedCode,
|
|
345136
346236
|
baseUrl: run2.baseUrl,
|
|
345137
346237
|
testVariables: run2.testVariables,
|
|
345138
|
-
|
|
346238
|
+
storageStatePath: apiHealStorageStatePath,
|
|
346239
|
+
validateContext: callbackContext
|
|
345139
346240
|
});
|
|
345140
346241
|
if (finalVerifyResult.passed) {
|
|
345141
346242
|
log.ok(` Attempt ${attempt} verification PASSED`);
|
|
@@ -345188,7 +346289,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345188
346289
|
if (classifyErrorMessage(composedError ?? null) === "config_issue") {
|
|
345189
346290
|
apiHealStatus = "ERROR";
|
|
345190
346291
|
} else {
|
|
345191
|
-
const rlDetection = (0,
|
|
346292
|
+
const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
|
|
345192
346293
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
345193
346294
|
tags: run2.tags ?? [],
|
|
345194
346295
|
error: composedError ?? finalVerifyResult?.error ?? null,
|
|
@@ -345285,8 +346386,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345285
346386
|
lastHealAttempt: run2.lastHealAttempt ?? null,
|
|
345286
346387
|
lastHealedCode: run2.lastHealedCode ?? null,
|
|
345287
346388
|
failureScreenshot: run2.failureScreenshot ?? null,
|
|
345288
|
-
onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }),
|
|
345289
|
-
validateContext:
|
|
346389
|
+
onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }, run2.claimEpoch),
|
|
346390
|
+
validateContext: callbackContext,
|
|
345290
346391
|
liveBrowserHandle,
|
|
345291
346392
|
runNativeTest: runNativeWithLiveBrowser
|
|
345292
346393
|
};
|
|
@@ -345328,7 +346429,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345328
346429
|
let healStatus = result2.passed ? "PASSED" : isConfigIssue ? "ERROR" : "FAILED";
|
|
345329
346430
|
let healError = result2.error;
|
|
345330
346431
|
if (!result2.passed && !isConfigIssue) {
|
|
345331
|
-
const rlDetection = (0,
|
|
346432
|
+
const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
|
|
345332
346433
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
345333
346434
|
tags: run2.tags ?? [],
|
|
345334
346435
|
error: result2.error,
|
|
@@ -345399,7 +346500,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345399
346500
|
// Allow generated `persistCredential(...)` calls inside the suite to
|
|
345400
346501
|
// POST back to the server with the runner's bearer token.
|
|
345401
346502
|
validateServerUrl: opts.serverUrl,
|
|
345402
|
-
validateRunnerAuthHeader: headers.Authorization
|
|
346503
|
+
validateRunnerAuthHeader: headers.Authorization,
|
|
346504
|
+
claimEpoch: run2.claimEpoch
|
|
345403
346505
|
});
|
|
345404
346506
|
const overall = result2.error && result2.testResults.length === 0 ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
|
|
345405
346507
|
const rr = await fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/regression-result`, {
|
|
@@ -345450,11 +346552,14 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345450
346552
|
});
|
|
345451
346553
|
if (healRes.ok) {
|
|
345452
346554
|
const { healedCode } = await healRes.json();
|
|
346555
|
+
const failedTestPlan = authPlanFor(failedTest.tags, healedCode);
|
|
345453
346556
|
const verifyResult = await runNativeWithLiveBrowser({
|
|
345454
346557
|
playwrightCode: healedCode,
|
|
345455
346558
|
baseUrl: run2.baseUrl,
|
|
345456
346559
|
testVariables: run2.testVariables,
|
|
345457
|
-
|
|
346560
|
+
storageStatePath: failedTestPlan.needsCookieState ? (0, import_runner_core12.getValidLocalAuthState)(runAuthCachePath) : void 0,
|
|
346561
|
+
authStandingMode: failedTestPlan.standingHeadersMode,
|
|
346562
|
+
validateContext: callbackContext
|
|
345458
346563
|
});
|
|
345459
346564
|
results.push({
|
|
345460
346565
|
testCaseId: failedTest.testCaseId,
|
|
@@ -345517,8 +346622,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345517
346622
|
onAICall: healAudit,
|
|
345518
346623
|
modelOverride: run2.healerModel,
|
|
345519
346624
|
consecutiveFails: failedTest.consecutiveFails,
|
|
345520
|
-
onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }),
|
|
345521
|
-
validateContext:
|
|
346625
|
+
onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }, run2.claimEpoch),
|
|
346626
|
+
validateContext: callbackContext,
|
|
345522
346627
|
liveBrowserHandle,
|
|
345523
346628
|
runNativeTest: runNativeWithLiveBrowser
|
|
345524
346629
|
};
|
|
@@ -345723,6 +346828,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
345723
346828
|
}
|
|
345724
346829
|
return;
|
|
345725
346830
|
}
|
|
346831
|
+
ctx.onCredentialUpdate = async (input) => postCredentialRotation(
|
|
346832
|
+
opts.serverUrl,
|
|
346833
|
+
headers,
|
|
346834
|
+
run2.runId,
|
|
346835
|
+
{ ...input, actor: "ai-explorer" },
|
|
346836
|
+
run2.claimEpoch
|
|
346837
|
+
);
|
|
345726
346838
|
log.info(`Discovery (${ctx.mode}) on ${ctx.baseUrl}`);
|
|
345727
346839
|
const { push: pushAudit, drain: drainAudit } = createAuditFlusher(
|
|
345728
346840
|
`${opts.serverUrl}/api/runner/runs/${run2.runId}/ai-audit-batch`,
|
|
@@ -346425,7 +347537,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346425
347537
|
featureDescription: ctx.featureDescription,
|
|
346426
347538
|
featurePages: ctx.featurePages,
|
|
346427
347539
|
appBrief: ctx.appBrief,
|
|
346428
|
-
onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-capture" })
|
|
347540
|
+
onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-capture" }, run2.claimEpoch)
|
|
346429
347541
|
});
|
|
346430
347542
|
if (logBuffer.length > 0) {
|
|
346431
347543
|
postCaptureLog(opts.serverUrl, headers, run2.runId, ctx.jobId, truncateTextTail(logBuffer.join("\n"), DISCOVERY_LOG_POST_MAX_CHARS, "capture log"));
|
|
@@ -346670,7 +347782,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346670
347782
|
}
|
|
346671
347783
|
return verify;
|
|
346672
347784
|
};
|
|
346673
|
-
return await (0,
|
|
347785
|
+
return await (0, import_runner_core14.executeMobileLiveHeal)({
|
|
346674
347786
|
steps: a.steps,
|
|
346675
347787
|
target: a.target,
|
|
346676
347788
|
testVariables: a.testVariables,
|
|
@@ -346687,7 +347799,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346687
347799
|
}
|
|
346688
347800
|
};
|
|
346689
347801
|
log.info("\u{1FA7A} Mobile AI heal \u2014 Phase 1 (script) \u2192 Phase 2 (live device, lazy)...");
|
|
346690
|
-
const heal = await (0,
|
|
347802
|
+
const heal = await (0, import_runner_core14.executeMobileTestWithHealing)(
|
|
346691
347803
|
mobileSteps,
|
|
346692
347804
|
healTarget,
|
|
346693
347805
|
{
|
|
@@ -346700,7 +347812,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
346700
347812
|
healerOptions
|
|
346701
347813
|
);
|
|
346702
347814
|
const finalRes = nativeVerify.last;
|
|
346703
|
-
const outcome = (0,
|
|
347815
|
+
const outcome = (0, import_runner_core14.computeMobileHealOutcome)({
|
|
346704
347816
|
heal,
|
|
346705
347817
|
finalError: finalRes?.error ?? null,
|
|
346706
347818
|
reproduceError,
|
|
@@ -346846,26 +347958,77 @@ ${result2.logs}`);
|
|
|
346846
347958
|
}
|
|
346847
347959
|
return;
|
|
346848
347960
|
}
|
|
346849
|
-
const
|
|
346850
|
-
|
|
346851
|
-
|
|
346852
|
-
|
|
346853
|
-
|
|
346854
|
-
|
|
347961
|
+
const projectId = run2.projectId || opts.projectId;
|
|
347962
|
+
const authStateExpected = shouldUseAuthState(run2.tags, run2.playwrightCode);
|
|
347963
|
+
const captureAuth = shouldCaptureAuthState(run2.tags, run2.playwrightCode);
|
|
347964
|
+
const authPaths = authStateExpected || captureAuth ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
|
|
347965
|
+
projectId,
|
|
347966
|
+
serverUrl: opts.serverUrl,
|
|
346855
347967
|
baseUrl: run2.baseUrl,
|
|
346856
347968
|
testVariables: run2.testVariables,
|
|
346857
|
-
|
|
346858
|
-
|
|
346859
|
-
|
|
346860
|
-
|
|
346861
|
-
|
|
347969
|
+
authToken: opts.authToken
|
|
347970
|
+
}, run2.runId) : void 0;
|
|
347971
|
+
let releaseAuthLock;
|
|
347972
|
+
let storageStatePath;
|
|
347973
|
+
if (authPaths && authStateExpected) {
|
|
347974
|
+
releaseAuthLock = await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths.lockKey, authPaths.lockPath);
|
|
347975
|
+
storageStatePath = (0, import_runner_core12.getValidLocalAuthState)(authPaths.cachePath);
|
|
347976
|
+
if (storageStatePath || !captureAuth) {
|
|
347977
|
+
releaseAuthLock();
|
|
347978
|
+
releaseAuthLock = void 0;
|
|
347979
|
+
}
|
|
347980
|
+
if (!storageStatePath && !captureAuth && sharedAuthInjectEnabled()) {
|
|
347981
|
+
log.info(`Waiting for the auth producer to warm project ${projectId}`);
|
|
347982
|
+
storageStatePath = await (0, import_runner_core12.waitForLocalAuthStateProducer)(
|
|
347983
|
+
authPaths.cachePath,
|
|
347984
|
+
authPaths.lockKey,
|
|
347985
|
+
2e3,
|
|
347986
|
+
void 0,
|
|
347987
|
+
100,
|
|
347988
|
+
authPaths.lockPath
|
|
347989
|
+
);
|
|
346862
347990
|
}
|
|
346863
|
-
}
|
|
347991
|
+
}
|
|
347992
|
+
if (storageStatePath) {
|
|
347993
|
+
log.info(`Using auth state for project ${projectId}`);
|
|
347994
|
+
}
|
|
347995
|
+
const runAuthPath = captureAuth ? authPaths?.capturePath : void 0;
|
|
347996
|
+
const result = await (async () => {
|
|
347997
|
+
try {
|
|
347998
|
+
return await runNativeWithLiveBrowser({
|
|
347999
|
+
playwrightCode: run2.playwrightCode,
|
|
348000
|
+
baseUrl: run2.baseUrl,
|
|
348001
|
+
testVariables: run2.testVariables,
|
|
348002
|
+
authStateCapturePath: runAuthPath,
|
|
348003
|
+
storageStatePath,
|
|
348004
|
+
validateContext: callbackContext
|
|
348005
|
+
});
|
|
348006
|
+
} finally {
|
|
348007
|
+
if (runAuthPath && authPaths) {
|
|
348008
|
+
if ((0, import_runner_core12.getValidLocalAuthState)(runAuthPath)) {
|
|
348009
|
+
const releasePromotionLock = releaseAuthLock ? void 0 : await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths.lockKey, authPaths.lockPath);
|
|
348010
|
+
try {
|
|
348011
|
+
const captured = (0, import_runner_core12.promoteLocalAuthStateCapture)(
|
|
348012
|
+
runAuthPath,
|
|
348013
|
+
authPaths.cachePath,
|
|
348014
|
+
!!storageStatePath,
|
|
348015
|
+
releaseAuthLock ?? releasePromotionLock
|
|
348016
|
+
);
|
|
348017
|
+
if (captured) log.info(`Captured auth state for project ${projectId}`);
|
|
348018
|
+
} finally {
|
|
348019
|
+
releasePromotionLock?.();
|
|
348020
|
+
}
|
|
348021
|
+
}
|
|
348022
|
+
(0, import_runner_core12.discardLocalAuthStateCapture)(runAuthPath);
|
|
348023
|
+
}
|
|
348024
|
+
releaseAuthLock?.();
|
|
348025
|
+
}
|
|
348026
|
+
})();
|
|
346864
348027
|
const duration = Date.now() - startTime;
|
|
346865
348028
|
let runStatus = result.passed ? "PASSED" : "FAILED";
|
|
346866
348029
|
let runError = result.error;
|
|
346867
348030
|
if (!result.passed) {
|
|
346868
|
-
const rlDetection = (0,
|
|
348031
|
+
const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
|
|
346869
348032
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
346870
348033
|
tags: run2.tags ?? [],
|
|
346871
348034
|
error: result.error,
|
|
@@ -346879,6 +348042,10 @@ ${result2.logs}`);
|
|
|
346879
348042
|
log.warn(`\u26A0 Rate-limit block detected - posting ERROR (not FAILED) to skip heal/quarantine`);
|
|
346880
348043
|
}
|
|
346881
348044
|
}
|
|
348045
|
+
if (runStatus === "FAILED" && classifyErrorMessage(runError ?? null) === "config_issue") {
|
|
348046
|
+
runStatus = "ERROR";
|
|
348047
|
+
log.warn(`\u26A0 CONFIG_ISSUE from native execution - posting ERROR (not FAILED) to skip heal/quarantine`);
|
|
348048
|
+
}
|
|
346882
348049
|
await postResult(opts.serverUrl, headers, run2.runId, {
|
|
346883
348050
|
status: runStatus,
|
|
346884
348051
|
stepResults: result.stepResults,
|
|
@@ -346894,7 +348061,7 @@ ${result2.logs}`);
|
|
|
346894
348061
|
if (result.passed) {
|
|
346895
348062
|
log.ok(`PASSED (${duration}ms)${result.suspiciousPass ? ` \u26A0 suspicious-pass (viewport ${Math.round(result.suspiciousPass.emptyRatio * 100)}% blank)` : ""}`);
|
|
346896
348063
|
} else if (runStatus === "ERROR") {
|
|
346897
|
-
log.warn(`BLOCKED (${duration}ms) - rate-limit
|
|
348064
|
+
log.warn(`BLOCKED (${duration}ms) - environmental block (rate-limit / config-issue), not counted as failure`);
|
|
346898
348065
|
} else {
|
|
346899
348066
|
log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
|
|
346900
348067
|
if (result.logs) {
|
|
@@ -346926,6 +348093,12 @@ async function startRunner(opts) {
|
|
|
346926
348093
|
process.env.AUTH_TOKEN = authToken;
|
|
346927
348094
|
process.env.RUNNER_TOKEN = authToken;
|
|
346928
348095
|
if (projectId) process.env.PROJECT_ID = projectId;
|
|
348096
|
+
(0, import_runner_core12.cleanupExpiredLocalAuthStateFiles)(getRunsDir());
|
|
348097
|
+
const authCleanupTimer = setInterval(
|
|
348098
|
+
() => (0, import_runner_core12.cleanupExpiredLocalAuthStateFiles)(getRunsDir()),
|
|
348099
|
+
import_runner_core12.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS
|
|
348100
|
+
);
|
|
348101
|
+
authCleanupTimer.unref?.();
|
|
346929
348102
|
const headers = buildHeaders(authToken, projectId);
|
|
346930
348103
|
console.log("");
|
|
346931
348104
|
console.log(`${C2.bold}${C2.cyan}validate.qa Local Runner${C2.reset}`);
|
|
@@ -347034,7 +348207,7 @@ async function startRunner(opts) {
|
|
|
347034
348207
|
capture: envInt("MAX_CAPTURE", DEFAULT_TYPE_LIMITS.capture),
|
|
347035
348208
|
run: envInt("MAX_RUN", DEFAULT_TYPE_LIMITS.run)
|
|
347036
348209
|
};
|
|
347037
|
-
const RESOURCE_GOVERNOR_OPTIONS = (0,
|
|
348210
|
+
const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core13.createResourceGovernorOptions)({
|
|
347038
348211
|
isCloud: IS_CLOUD,
|
|
347039
348212
|
maxConcurrent: MAX_CONCURRENT,
|
|
347040
348213
|
typeLimits: TYPE_LIMITS
|
|
@@ -347202,8 +348375,8 @@ async function startRunner(opts) {
|
|
|
347202
348375
|
android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
|
|
347203
348376
|
} : { web: true, ios: false, android: false };
|
|
347204
348377
|
const bridge = enableMobile ? getBridgeState() : void 0;
|
|
347205
|
-
const resourceStatus = (0,
|
|
347206
|
-
const processInventory = await (0,
|
|
348378
|
+
const resourceStatus = (0, import_runner_core13.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
|
|
348379
|
+
const processInventory = await (0, import_runner_core13.getBrowserProcessInventory)();
|
|
347207
348380
|
lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
|
|
347208
348381
|
lastInventoryKinds.clear();
|
|
347209
348382
|
for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
|
|
@@ -347233,7 +348406,7 @@ async function startRunner(opts) {
|
|
|
347233
348406
|
resourceSnapshot: resourceStatus.snapshot,
|
|
347234
348407
|
resourceGovernor: resourceStatus.governor,
|
|
347235
348408
|
processInventory,
|
|
347236
|
-
aiQueue: (0,
|
|
348409
|
+
aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
|
|
347237
348410
|
liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
|
|
347238
348411
|
// Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
|
|
347239
348412
|
// Cached & cheap — see getCachedDoctor above.
|
|
@@ -347344,7 +348517,7 @@ async function startRunner(opts) {
|
|
|
347344
348517
|
let pidsWatchdogWarned = false;
|
|
347345
348518
|
const pidsWatchdog = setInterval(() => {
|
|
347346
348519
|
if (shuttingDown) return;
|
|
347347
|
-
const snapshot = (0,
|
|
348520
|
+
const snapshot = (0, import_runner_core13.getResourceSnapshot)();
|
|
347348
348521
|
const pct = snapshot.pidsPct;
|
|
347349
348522
|
if (pct == null || !Number.isFinite(pct)) return;
|
|
347350
348523
|
if (pct >= PIDS_RECYCLE_PCT) {
|
|
@@ -347384,7 +348557,7 @@ async function startRunner(opts) {
|
|
|
347384
348557
|
log.dim(` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
|
|
347385
348558
|
continue;
|
|
347386
348559
|
}
|
|
347387
|
-
const admission = (0,
|
|
348560
|
+
const admission = (0, import_runner_core13.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
|
|
347388
348561
|
mode: runMode,
|
|
347389
348562
|
activeRuns,
|
|
347390
348563
|
activeByType: Object.fromEntries(activeByType),
|
|
@@ -347462,9 +348635,9 @@ async function startRunner(opts) {
|
|
|
347462
348635
|
leasedDeviceId = freeDeviceId;
|
|
347463
348636
|
leasedDeviceIds.set(freeDeviceId, run2.runId);
|
|
347464
348637
|
}
|
|
347465
|
-
let
|
|
348638
|
+
let claimEpoch = false;
|
|
347466
348639
|
try {
|
|
347467
|
-
|
|
348640
|
+
claimEpoch = await claimRun(serverUrl, headers, run2.runId);
|
|
347468
348641
|
} catch (err) {
|
|
347469
348642
|
if (leasedDeviceId) {
|
|
347470
348643
|
leasedDeviceIds.delete(leasedDeviceId);
|
|
@@ -347472,7 +348645,7 @@ async function startRunner(opts) {
|
|
|
347472
348645
|
}
|
|
347473
348646
|
throw err;
|
|
347474
348647
|
}
|
|
347475
|
-
if (
|
|
348648
|
+
if (claimEpoch === false) {
|
|
347476
348649
|
log.dim(` Skipped ${run2.runId} (already claimed)`);
|
|
347477
348650
|
if (leasedDeviceId) {
|
|
347478
348651
|
leasedDeviceIds.delete(leasedDeviceId);
|
|
@@ -347480,6 +348653,7 @@ async function startRunner(opts) {
|
|
|
347480
348653
|
}
|
|
347481
348654
|
continue;
|
|
347482
348655
|
}
|
|
348656
|
+
run2.claimEpoch = claimEpoch;
|
|
347483
348657
|
activeRuns++;
|
|
347484
348658
|
incrementType(runMode);
|
|
347485
348659
|
incrementProject(runProject);
|