@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.
Files changed (3) hide show
  1. package/dist/cli.js +1352 -178
  2. package/dist/cli.mjs +1352 -178
  3. package/package.json +2 -2
package/dist/cli.mjs CHANGED
@@ -1761,7 +1761,18 @@ var init_constants = __esm({
1761
1761
  "MINIMAX_API_KEY",
1762
1762
  "ANTHROPIC_API_KEY",
1763
1763
  "NVIDIA_API_KEY",
1764
- "DEEPSEEK_API_KEY"
1764
+ "DEEPSEEK_API_KEY",
1765
+ // Run-scoped callback capabilities. The parent runner injects these after
1766
+ // sanitization; customer variables must never impersonate the control plane.
1767
+ "AUTH_FILE",
1768
+ "SHARED_AUTH_INJECT",
1769
+ "VALIDATE_RUN_ID",
1770
+ "VALIDATE_CLAIM_EPOCH",
1771
+ "VALIDATE_AUTH_CONFIG_HASH",
1772
+ "VALIDATE_TASK_ARN",
1773
+ "VALIDATE_RUNNER_AUTH",
1774
+ "VALIDATE_SERVER_URL",
1775
+ "VALIDATE_LIVE_BROWSER_STATE_PATH"
1765
1776
  ]);
1766
1777
  SPAWNED_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
1767
1778
  // Runtime essentials - Node.js and Playwright need these to function
@@ -4773,6 +4784,204 @@ var init_origin_domain = __esm({
4773
4784
  }
4774
4785
  });
4775
4786
 
4787
+ // ../shared/dist/auth-plan.js
4788
+ function hasInlineLoginSteps(playwrightCode) {
4789
+ if (!playwrightCode)
4790
+ return false;
4791
+ return /process\.env\.TEST_USER_EMAIL!?/.test(playwrightCode) && /process\.env\.TEST_USER_PASSWORD!?/.test(playwrightCode);
4792
+ }
4793
+ function hasGeneratedAuthGuard(playwrightCode) {
4794
+ if (!playwrightCode)
4795
+ return false;
4796
+ return /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
4797
+ }
4798
+ function legacyNeedsCookieState(tags, code, sharedAuthInjectEnabled2) {
4799
+ if (tags.includes("@auth"))
4800
+ return false;
4801
+ if (tags.includes("@api"))
4802
+ return true;
4803
+ if (!tags.includes("@needs-auth"))
4804
+ return false;
4805
+ if (sharedAuthInjectEnabled2 && tags.includes("@auth-guarded") && hasGeneratedAuthGuard(code))
4806
+ return true;
4807
+ const isGeneratedVariant = tags.includes("@edge") || tags.includes("@bug");
4808
+ const hasLegacyInlineLogin = tags.includes("@discovered") && !isGeneratedVariant && hasInlineLoginSteps(code);
4809
+ return !tags.includes("@inline-login") && !hasLegacyInlineLogin;
4810
+ }
4811
+ function explicitAuthTypeTags(t) {
4812
+ const found = /* @__PURE__ */ new Set();
4813
+ for (const tag of t) {
4814
+ if (AUTH_TYPE_TAGS.includes(tag))
4815
+ found.add(tag);
4816
+ else if (AUTH_TAG_ALIASES[tag])
4817
+ found.add(AUTH_TAG_ALIASES[tag]);
4818
+ }
4819
+ return [...found];
4820
+ }
4821
+ function invalidPlan(configurationError) {
4822
+ return {
4823
+ version: 1,
4824
+ source: "invalid",
4825
+ acquisition: "unsupported",
4826
+ presentation: "none",
4827
+ needsCookieState: false,
4828
+ needsStandingHeaders: false,
4829
+ standingHeadersMode: "none",
4830
+ needsLoginCreds: false,
4831
+ requiredEnvKeys: [],
4832
+ configurationError
4833
+ };
4834
+ }
4835
+ function explicitPlan(authTypeTag2, acquisition, presentation, opts) {
4836
+ return {
4837
+ version: 1,
4838
+ source: "explicit",
4839
+ authTypeTag: authTypeTag2,
4840
+ acquisition,
4841
+ presentation,
4842
+ needsCookieState: opts.cookie,
4843
+ needsStandingHeaders: opts.standingMode === "required",
4844
+ standingHeadersMode: opts.standingMode,
4845
+ needsLoginCreds: TEST_USER_ENV_KEYS.every((k) => opts.requiredEnvKeys.includes(k)),
4846
+ requiredEnvKeys: opts.requiredEnvKeys
4847
+ };
4848
+ }
4849
+ function deriveAuthPlan(tags, code, opts) {
4850
+ const t = Array.isArray(tags) ? tags : [];
4851
+ const sharedFlag = opts?.sharedAuthInjectEnabled ?? false;
4852
+ const isApi = t.includes("@api");
4853
+ const explicit = isApi ? explicitAuthTypeTags(t) : [];
4854
+ if (explicit.length > 0) {
4855
+ if (t.includes("@auth")) {
4856
+ return invalidPlan("@auth (self-login UI) cannot be combined with an explicit @api auth-type tag");
4857
+ }
4858
+ if (explicit.length > 1) {
4859
+ return invalidPlan(`conflicting auth-type tags: ${explicit.join(", ")}`);
4860
+ }
4861
+ switch (explicit[0]) {
4862
+ case AUTH_TAG_UNSUPPORTED:
4863
+ return {
4864
+ ...invalidPlan("auth scheme not yet drivable (e.g. HMAC / mTLS / provider-replay) \u2014 needs manual configuration")
4865
+ };
4866
+ case AUTH_TAG_NONE:
4867
+ return explicitPlan(AUTH_TAG_NONE, "none", "none", { cookie: false, standingMode: "none", requiredEnvKeys: [] });
4868
+ case AUTH_TAG_STANDING:
4869
+ return explicitPlan(AUTH_TAG_STANDING, "standing-secret", "standing-header", { cookie: false, standingMode: "required", requiredEnvKeys: [API_AUTH_HEADERS_ENV_KEY] });
4870
+ case AUTH_TAG_LOGIN_BEARER:
4871
+ return explicitPlan(AUTH_TAG_LOGIN_BEARER, "request-login", "bearer", { cookie: false, standingMode: "none", requiredEnvKeys: [...TEST_USER_ENV_KEYS] });
4872
+ case AUTH_TAG_LOGIN_COOKIE:
4873
+ return explicitPlan(AUTH_TAG_LOGIN_COOKIE, "request-login", "cookie", { cookie: false, standingMode: "legacy-if-configured", requiredEnvKeys: [...TEST_USER_ENV_KEYS] });
4874
+ case AUTH_TAG_COOKIE_STATE:
4875
+ return explicitPlan(AUTH_TAG_COOKIE_STATE, "browser-bootstrap", "cookie", { cookie: true, standingMode: "legacy-if-configured", requiredEnvKeys: [...TEST_USER_ENV_KEYS] });
4876
+ }
4877
+ }
4878
+ const cookie = legacyNeedsCookieState(t, code, sharedFlag);
4879
+ const authedLegacy = t.includes("@auth") || t.includes("@needs-auth") || isApi;
4880
+ return {
4881
+ version: 1,
4882
+ source: "legacy",
4883
+ acquisition: cookie ? "browser-bootstrap" : authedLegacy ? "request-login" : "none",
4884
+ presentation: authedLegacy ? "cookie" : "none",
4885
+ needsCookieState: cookie,
4886
+ needsStandingHeaders: false,
4887
+ // A legacy bare-@api test keeps injecting standing headers IF the project configured them
4888
+ // (preserving today's behavior) — but does not require them. Non-@api legacy tests never inject.
4889
+ standingHeadersMode: isApi ? "legacy-if-configured" : "none",
4890
+ needsLoginCreds: authedLegacy,
4891
+ requiredEnvKeys: authedLegacy ? [...TEST_USER_ENV_KEYS] : []
4892
+ };
4893
+ }
4894
+ function authTypeTag(tags) {
4895
+ const t = Array.isArray(tags) ? tags : [];
4896
+ const explicit = explicitAuthTypeTags(t);
4897
+ return explicit.length === 1 ? explicit[0] : void 0;
4898
+ }
4899
+ var AUTH_TAG_COOKIE_STATE, AUTH_TAG_LOGIN_COOKIE, AUTH_TAG_LOGIN_BEARER, AUTH_TAG_STANDING, AUTH_TAG_NONE, AUTH_TAG_UNSUPPORTED, AUTH_TYPE_TAGS, AUTH_TAG_ALIASES, TEST_USER_ENV_KEYS, API_AUTH_HEADERS_ENV_KEY;
4900
+ var init_auth_plan = __esm({
4901
+ "../shared/dist/auth-plan.js"() {
4902
+ "use strict";
4903
+ AUTH_TAG_COOKIE_STATE = "@auth-cookie-state";
4904
+ AUTH_TAG_LOGIN_COOKIE = "@auth-login-cookie";
4905
+ AUTH_TAG_LOGIN_BEARER = "@auth-login-bearer";
4906
+ AUTH_TAG_STANDING = "@auth-standing";
4907
+ AUTH_TAG_NONE = "@auth-none";
4908
+ AUTH_TAG_UNSUPPORTED = "@auth-unsupported";
4909
+ AUTH_TYPE_TAGS = [
4910
+ AUTH_TAG_COOKIE_STATE,
4911
+ AUTH_TAG_LOGIN_COOKIE,
4912
+ AUTH_TAG_LOGIN_BEARER,
4913
+ AUTH_TAG_STANDING,
4914
+ AUTH_TAG_NONE,
4915
+ AUTH_TAG_UNSUPPORTED
4916
+ ];
4917
+ AUTH_TAG_ALIASES = {
4918
+ "@auth-cookie": AUTH_TAG_COOKIE_STATE,
4919
+ "@auth-bearer": AUTH_TAG_LOGIN_BEARER,
4920
+ "@auth-token": AUTH_TAG_LOGIN_BEARER,
4921
+ "@auth-api-key": AUTH_TAG_STANDING,
4922
+ "@auth-standing-header": AUTH_TAG_STANDING
4923
+ };
4924
+ TEST_USER_ENV_KEYS = ["TEST_USER_EMAIL", "TEST_USER_PASSWORD"];
4925
+ API_AUTH_HEADERS_ENV_KEY = "API_AUTH_HEADERS";
4926
+ }
4927
+ });
4928
+
4929
+ // ../shared/dist/redact-needles.js
4930
+ function escapeRegExp(s) {
4931
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4932
+ }
4933
+ function buildNeedleMatcher(needles) {
4934
+ const usable = Array.from(new Set(needles.filter((n) => typeof n === "string" && n.length >= MIN_NEEDLE_LENGTH))).sort((a, b) => b.length - a.length);
4935
+ if (usable.length === 0)
4936
+ return null;
4937
+ return new RegExp(usable.map(escapeRegExp).join("|"), "g");
4938
+ }
4939
+ function redactNeedlesText(text, matcher) {
4940
+ if (!matcher || !text)
4941
+ return text;
4942
+ matcher.lastIndex = 0;
4943
+ const out = text.replace(matcher, REDACTED);
4944
+ return out === text ? text : out;
4945
+ }
4946
+ function redactNeedlesDeep(value, matcher) {
4947
+ if (!matcher)
4948
+ return value;
4949
+ if (typeof value === "string") {
4950
+ const scrubbed = redactNeedlesText(value, matcher);
4951
+ return scrubbed === value ? value : scrubbed;
4952
+ }
4953
+ if (Array.isArray(value)) {
4954
+ let changed = false;
4955
+ const next = value.map((item) => {
4956
+ const scrubbed = redactNeedlesDeep(item, matcher);
4957
+ if (scrubbed !== item)
4958
+ changed = true;
4959
+ return scrubbed;
4960
+ });
4961
+ return changed ? next : value;
4962
+ }
4963
+ if (value !== null && typeof value === "object") {
4964
+ let changed = false;
4965
+ const next = {};
4966
+ for (const [k, v] of Object.entries(value)) {
4967
+ const scrubbed = redactNeedlesDeep(v, matcher);
4968
+ if (scrubbed !== v)
4969
+ changed = true;
4970
+ next[k] = scrubbed;
4971
+ }
4972
+ return changed ? next : value;
4973
+ }
4974
+ return value;
4975
+ }
4976
+ var REDACTED, MIN_NEEDLE_LENGTH;
4977
+ var init_redact_needles = __esm({
4978
+ "../shared/dist/redact-needles.js"() {
4979
+ "use strict";
4980
+ REDACTED = "[REDACTED]";
4981
+ MIN_NEEDLE_LENGTH = 8;
4982
+ }
4983
+ });
4984
+
4776
4985
  // ../shared/dist/voice-config.js
4777
4986
  function normalizeTtsVoiceId(voiceId) {
4778
4987
  if (!voiceId)
@@ -5029,6 +5238,16 @@ var init_voice_config = __esm({
5029
5238
  }
5030
5239
  });
5031
5240
 
5241
+ // ../shared/dist/feature-flags.js
5242
+ function isSharedAuthInjectEnabled(value = process.env.SHARED_AUTH_INJECT) {
5243
+ return value?.trim().toLowerCase() !== "false";
5244
+ }
5245
+ var init_feature_flags = __esm({
5246
+ "../shared/dist/feature-flags.js"() {
5247
+ "use strict";
5248
+ }
5249
+ });
5250
+
5032
5251
  // ../shared/dist/index.js
5033
5252
  var dist_exports = {};
5034
5253
  __export(dist_exports, {
@@ -5037,8 +5256,16 @@ __export(dist_exports, {
5037
5256
  ADDON_DEFINITIONS: () => ADDON_DEFINITIONS,
5038
5257
  ADMIN_EMAIL_LIMITS: () => ADMIN_EMAIL_LIMITS,
5039
5258
  AI_CREDIT_MARGIN: () => AI_CREDIT_MARGIN,
5259
+ API_AUTH_HEADERS_ENV_KEY: () => API_AUTH_HEADERS_ENV_KEY,
5040
5260
  API_PREFIX: () => API_PREFIX,
5041
5261
  AUDIO_CHUNK_DURATION_MS: () => AUDIO_CHUNK_DURATION_MS,
5262
+ AUTH_TAG_COOKIE_STATE: () => AUTH_TAG_COOKIE_STATE,
5263
+ AUTH_TAG_LOGIN_BEARER: () => AUTH_TAG_LOGIN_BEARER,
5264
+ AUTH_TAG_LOGIN_COOKIE: () => AUTH_TAG_LOGIN_COOKIE,
5265
+ AUTH_TAG_NONE: () => AUTH_TAG_NONE,
5266
+ AUTH_TAG_STANDING: () => AUTH_TAG_STANDING,
5267
+ AUTH_TAG_UNSUPPORTED: () => AUTH_TAG_UNSUPPORTED,
5268
+ AUTH_TYPE_TAGS: () => AUTH_TYPE_TAGS,
5042
5269
  COST_PER_M: () => COST_PER_M,
5043
5270
  CREDIT_PACKS_CENTS: () => CREDIT_PACKS_CENTS,
5044
5271
  CREDIT_PRICES_CENTS: () => CREDIT_PRICES_CENTS,
@@ -5081,6 +5308,7 @@ __export(dist_exports, {
5081
5308
  STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
5082
5309
  STT_PROVIDER_OPTIONS: () => STT_PROVIDER_OPTIONS,
5083
5310
  SUPPORT_EMAIL: () => SUPPORT_EMAIL,
5311
+ TEST_USER_ENV_KEYS: () => TEST_USER_ENV_KEYS,
5084
5312
  TRANSIENT_CONTROL_ROLES: () => TRANSIENT_CONTROL_ROLES,
5085
5313
  TRIAL_DURATION_DAYS: () => TRIAL_DURATION_DAYS,
5086
5314
  TTS_MODEL_OPTIONS: () => TTS_MODEL_OPTIONS,
@@ -5090,10 +5318,12 @@ __export(dist_exports, {
5090
5318
  accumulateStatements: () => accumulateStatements,
5091
5319
  aiCostToCreditCents: () => aiCostToCreditCents,
5092
5320
  analyzeMobileSelectorQuality: () => analyzeMobileSelectorQuality,
5321
+ authTypeTag: () => authTypeTag,
5093
5322
  bugFoundQuarantineWhere: () => bugFoundQuarantineWhere,
5094
5323
  buildAppiumCode: () => buildAppiumCode,
5095
5324
  buildLaunchStep: () => buildLaunchStep,
5096
5325
  buildMobileStep: () => buildMobileStep,
5326
+ buildNeedleMatcher: () => buildNeedleMatcher,
5097
5327
  buildSandboxedEnv: () => buildSandboxedEnv,
5098
5328
  buildSanitizedAgentEnv: () => buildSanitizedAgentEnv,
5099
5329
  buildStepTarget: () => buildStepTarget,
@@ -5109,6 +5339,7 @@ __export(dist_exports, {
5109
5339
  controlSignature: () => controlSignature,
5110
5340
  countInnerTests: () => countInnerTests,
5111
5341
  decideHarPosted: () => decideHarPosted,
5342
+ deriveAuthPlan: () => deriveAuthPlan,
5112
5343
  deriveProjectKey: () => deriveProjectKey,
5113
5344
  detectCrossOriginHtmlAsJson: () => detectCrossOriginHtmlAsJson,
5114
5345
  detectsErrorBannerAssertion: () => detectsErrorBannerAssertion,
@@ -5152,6 +5383,7 @@ __export(dist_exports, {
5152
5383
  isRunnableStep: () => isRunnableStep,
5153
5384
  isSafeDeepLink: () => isSafeDeepLink,
5154
5385
  isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
5386
+ isSharedAuthInjectEnabled: () => isSharedAuthInjectEnabled,
5155
5387
  isSttModelForProvider: () => isSttModelForProvider,
5156
5388
  isTestReadyForPlatform: () => isTestReadyForPlatform,
5157
5389
  isTrialExpired: () => isTrialExpired,
@@ -5175,6 +5407,8 @@ __export(dist_exports, {
5175
5407
  planHasBillingPortal: () => planHasBillingPortal,
5176
5408
  planRequiresCheckout: () => planRequiresCheckout,
5177
5409
  platformTag: () => platformTag,
5410
+ redactNeedlesDeep: () => redactNeedlesDeep,
5411
+ redactNeedlesText: () => redactNeedlesText,
5178
5412
  registrableDomain: () => registrableDomain,
5179
5413
  resolveCrossOriginConfigOverride: () => resolveCrossOriginConfigOverride,
5180
5414
  resolveMobileStepValuePlaceholders: () => resolveMobileStepValuePlaceholders,
@@ -5217,7 +5451,10 @@ var init_dist = __esm({
5217
5451
  init_agent_env();
5218
5452
  init_test_counting();
5219
5453
  init_origin_domain();
5454
+ init_auth_plan();
5455
+ init_redact_needles();
5220
5456
  init_voice_config();
5457
+ init_feature_flags();
5221
5458
  }
5222
5459
  });
5223
5460
 
@@ -10681,7 +10918,7 @@ var require_transport = __commonJS({
10681
10918
  "../../node_modules/pino/lib/transport.js"(exports, module) {
10682
10919
  "use strict";
10683
10920
  var { createRequire } = __require("module");
10684
- var { existsSync: existsSync4 } = __require("fs");
10921
+ var { existsSync: existsSync3 } = __require("fs");
10685
10922
  var getCallers = require_caller();
10686
10923
  var { join: join7, isAbsolute, sep } = __require("path");
10687
10924
  var { fileURLToPath } = __require("url");
@@ -10755,7 +10992,7 @@ var require_transport = __commonJS({
10755
10992
  return false;
10756
10993
  }
10757
10994
  }
10758
- return isAbsolute(path) && !existsSync4(path);
10995
+ return isAbsolute(path) && !existsSync3(path);
10759
10996
  }
10760
10997
  function stripQuotes(value) {
10761
10998
  const first = value[0];
@@ -52383,11 +52620,11 @@ var require_leac = __commonJS({
52383
52620
  if (r.str.length === 0) {
52384
52621
  throw new Error(`Rule #${i} ("${r.name}") has empty "str" property, which is not allowed.`);
52385
52622
  }
52386
- return new RegExp(escapeRegExp(r.str), "y");
52623
+ return new RegExp(escapeRegExp2(r.str), "y");
52387
52624
  }
52388
- return new RegExp(escapeRegExp(r.name), "y");
52625
+ return new RegExp(escapeRegExp2(r.name), "y");
52389
52626
  }
52390
- function escapeRegExp(str) {
52627
+ function escapeRegExp2(str) {
52391
52628
  return str.replace(/[-[\]{}()*+!<=:?./\\^$|#\s,]/g, "\\$&");
52392
52629
  }
52393
52630
  function toSticky(re) {
@@ -68360,7 +68597,7 @@ var require_discover_ai_planner = __commonJS({
68360
68597
  "../runner-core/dist/services/discover-ai-planner.js"(exports) {
68361
68598
  "use strict";
68362
68599
  Object.defineProperty(exports, "__esModule", { value: true });
68363
- exports.SCENARIO_SYSTEM_PROMPT = void 0;
68600
+ exports.PLANNER_STRENGTHEN_GATE_PROMPT = exports.SCENARIO_SYSTEM_PROMPT = void 0;
68364
68601
  exports.getAIPlannerMaxTokens = getAIPlannerMaxTokens;
68365
68602
  exports.getSnapshotConfidence = getSnapshotConfidence;
68366
68603
  exports.compressSiteMap = compressSiteMap;
@@ -68943,7 +69180,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
68943
69180
  4. **Multi-page journeys** - only when transitions were actually observed
68944
69181
  5. **Bug reports** - for observed 500 errors, crashes, or broken features
68945
69182
  6. **Steps are human-readable** - "Fill the email field", not "page.getByLabel('Email').fill()"
68946
- 7. **Expected outcomes are plain-English success criteria** - describe the user-visible product result, not the controls touched along the way. Steps already cover buttons, inputs, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test. **Outcome-strength self-check (applies to EVERY scenario type):** before emitting a scenario, ask \u2014 would every expectedOutcome still pass if the primary action did nothing and only the page and its existing chrome rendered? If yes, it is too weak. Add at least one evidence-backed post-condition that would FAIL in that no-op case: specific result content or value, a created or removed entity, a changed count or status, persisted state after reload, handled validation copy, or a concrete session signal. A route alone suffices only when navigation or redirect is itself the behavior under test; a newly revealed control suffices only when revealing that control is the behavior under test. Never invent copy to satisfy this check \u2014 Rule 1 still governs what you may assert.
69183
+ 7. **Expected outcomes are plain-English success criteria** - describe the user-visible product result, not the controls touched along the way. Steps already cover buttons, inputs, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test. **Outcome-strength check (per outcome, applies to EVERY scenario type):** evaluate each expectedOutcome separately. Delete it if it would already be true immediately before the scenario's primary action \u2014 UNLESS navigation, redirect, or revealing that state is itself the behavior under test. Keep at least one evidence-backed discriminator that proves the primary behavior actually occurred: specific result content or value, a created or removed entity, a changed count or status, persisted state after reload, or handled validation copy. Every value, count-change, persistence, creation, or removal assertion MUST trace to an earlier evidence-backed step that establishes the baseline or exact target. If the evidence does not support such a step, drop the assertion; do not invent setup. **Silent-failure BUG_REPORT exception:** apply ONLY when evidence explicitly records an action as broken because its intended state change did not occur. It does NOT apply to expected validation, permission denial, unmet prerequisites, loading or rate-limit states, or any non-BUG scenario. Prove the unchanged state with an evidence-backed before/after discriminator tied to the attempted run-scoped target \u2014 not merely "no toast", "no redirect", or "the form is still visible". Never invent copy to satisfy this check \u2014 Rule 1 still governs what you may assert.
68947
69184
  8. **Auth-aware** - mark scenarios that need login. Use process.env.TEST_USER_EMAIL / PASSWORD
68948
69185
  9. **No duplicates** - check existing test names before adding
68949
69186
  10. **All tests start from the landing page** - every generated test navigates to \`/\` first and uses the app's own navigation (clicking links/buttons) to reach the target page. Steps must reflect this: say "Navigate to landing page and click Sign In" not "Navigate to /auth/login". Deep-linking to internal routes causes hydration failures in SPAs. \`targetPages\` lists the pages involved in the flow.
@@ -68956,7 +69193,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
68956
69193
  Example - BAD happy path: \`["Navigate to /feature", "Click Create", "Verify dialog opens"]\` \u2192 only tests the trigger.
68957
69194
  Example - GOOD happy path (generic, async): \`["Navigate to landing and log in", "Navigate to the feature page", "Note the current count of items on the list", "Click the primary action button", "Fill required inputs and submit", "Wait for completion signal (up to 60s)", "Verify the item count increased by 1", "Open the newly created item", "Verify its content is non-empty and contains the input I provided"]\`
68958
69195
  Example - GOOD happy path (sync CRUD): \`["Navigate to landing and log in", "Navigate to the list page", "Click New", "Fill required fields", "Click Save", "Verify the new record appears in the list", "Click the record and verify its detail view shows the values I entered"]\`
68959
- 12. **Auth happy paths require concrete session proof.** A login/auth HAPPY_PATH is incomplete if it only proves "user reaches dashboard" or "authenticated page is visible." It MUST verify at least one durable signed-in signal that was observed in the evidence: user/account menu opens, Logout/Sign out is available, the observed display name/email/avatar/account item is present, authenticated navigation remains available after a reload, or another app-specific session control is usable. Prefer concrete observed labels from the evidence (for example "Logout", "Sign out", "Account", "Profile", or the user's own displayed name/email from evidence) over generic phrases like "personalized content." If the explorer only observed a redirect and no signed-in signal, keep the route outcome but mark the session-proof outcome reliability "low" and state what concrete signal was missing.
69196
+ 12. **Auth happy paths require concrete session proof.** A login/auth HAPPY_PATH is incomplete if it only proves "user reaches dashboard" or "authenticated page is visible." It MUST verify at least one durable signed-in signal that was observed in the evidence: user/account menu opens, Logout/Sign out is available, the observed display name/email/avatar/account item is present, authenticated navigation remains available after a reload, or another app-specific session control is usable. Prefer concrete observed labels from the evidence (for example "Logout", "Sign out", "Account", "Profile", or the user's own displayed name/email from evidence) over generic phrases like "personalized content." If the explorer only observed a redirect and no concrete signed-in control, do NOT invent one or instruct the executor to "open the account/user menu" \u2014 assert only the exact observed redirect (plus any directly supported transition, e.g. the login form is no longer shown), treat the redirect and any observed transition as transition outcomes \u2014 not concrete session proof \u2014 assign each its evidence-supported reliability, and name the missing concrete signal as a coverage gap in reasoning. Never substitute a menu of generic alternatives ("such as Logout, Profile, or \u2026") for one evidenced signal in a step or expected outcome.
68960
69197
  13. **Independent execution contract.** Every scenario MUST pass when run alone, first, last, in random order, and in parallel against an environment with no relevant feature data except explicitly guaranteed immutable fixtures. A scenario that depends on a sibling scenario's side effects (e.g. "delete the conversation that another test created") is invalid. If two flows are inherently coupled, bundle them as a single USER_JOURNEY scenario.
68961
69198
  14. **Scenario-owned state.** If a scenario reads, searches, opens, updates, messages, cancels, or deletes an entity, that scenario MUST create or seed the entity in its own setup steps. "Use an existing record" / "the conversation in the demo account" / "the existing user named X" are forbidden phrasings \u2014 they assume mutable pre-existing app data. Carve-out: baseline observations (current count, list length, "the dashboard heading is visible") are allowed as prerequisite reads ONLY when the durable assertion targets a self-created entity. Phrase setup as: "Click New, fill required fields with run-scoped values, submit and verify the new record appears, THEN proceed to <feature under test>."
68962
69199
  15. **Run-scoped test data.** Generated entities MUST use unique, run-scoped identifiers \u2014 phrase as "test message <Date.now()>" or "QA-<unique id>" in steps. Subsequent assertions MUST target that unique identifier, not "the first item", "the latest message", "the last row", or any visible display name observed during exploration. Hardcoding any specific display name, username, email, conversation title, or other identifier observed in the explorer's snapshot is forbidden \u2014 that data is incidental, not a stable contract. The same identifiers will not exist (or will look different) when the test runs in a fresh environment or against a different account.
@@ -69019,6 +69256,9 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
69019
69256
  Citing observed labels in \`expectedOutcomes\` is fine \u2014 that's asserting on stable product signals, not implementation specifics.
69020
69257
 
69021
69258
  ## Coverage Strategy
69259
+ **Coverage allocation \u2014 order matters.** The test budget is a ceiling, never a target. Before writing JSON, privately inventory each distinct behavior the evidence explicitly RECORDS: a SUCCESS, EMPTY_STATE, VALIDATION_ERROR, ERROR, BUG, an Explorer-Recorded Journey outcome, or an observed page/state transition. Assign each recorded behavior to exactly one scenario as its primary coverage owner \u2014 this does not prohibit repeating evidence-backed prerequisite setup and verification required by Rules 13\u201317. Bundle input variants that exercise the same purpose (e.g. two malformed-parameter cases that both return the same validation) into that one scenario. Use Page Intelligence only to make sure every described surface is reached \u2014 a merely listed control, route, provider, tab, or input does NOT by itself prove a behavior or earn its own scenario. Do NOT add an inferred scenario just to fill the budget. If a recorded behavior cannot be tested independently without unsupported mutable state, name it as a coverage gap (rule 17) rather than pretending it is self-contained. Treat a page/state transition as a distinct behavior only when navigation or revealing that state is itself the recorded product outcome; otherwise keep it as a step within the owning scenario \u2014 Page Intelligence creates no additional coverage obligation.
69260
+
69261
+ Apply the per-surface minimums below only where the evidence actually supports them (evidence support includes Rule 1's trigger-only structural exception):
69022
69262
  - For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 11) + 1 EDGE_CASE (empty/invalid submission)
69023
69263
  - For auth flows: login happy path (verify the post-login route AND a concrete signed-in/session signal per rule 12) + validation (wrong password, empty fields)
69024
69264
  - For CRUD: each scenario MUST own the entity it operates on. Read/search/update/delete/cancel-on-existing/view-existing scenarios MUST include a create step earlier in the same scenario (or declare an immutable seed fixture by name). Never assume a record from prior runs or sibling tests exists. Splitting into atomic create/read/update/delete tests is fine ONLY when each test creates its own entity. When create/delete are tightly coupled (e.g. delete-only with no create flow elsewhere), bundle into a single USER_JOURNEY: create \u2192 verify \u2192 delete \u2192 verify removal.
@@ -69048,11 +69288,11 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
69048
69288
  { "action": "Fill the email field with test credentials" },
69049
69289
  { "action": "Fill the password field with test credentials" },
69050
69290
  { "action": "Click the Sign In button" },
69051
- { "action": "Open the observed authenticated account or user menu" }
69291
+ { "action": "After reaching the observed post-login page, reload it" }
69052
69292
  ],
69053
69293
  "expectedOutcomes": [
69054
- { "type": "url", "description": "User reaches /dashboard after signing in", "reliability": "high" },
69055
- { "type": "element-visible", "description": "The signed-in session is proven by an observed user/account signal such as Logout, Profile, or the user's displayed identity", "reliability": "high" }
69294
+ { "type": "url", "description": "User reaches /dashboard after signing in and remains there after reload", "reliability": "high" },
69295
+ { "type": "element-hidden", "description": "The login form does not return after reload", "reliability": "medium" }
69056
69296
  ],
69057
69297
  "targetPages": ["/auth/login", "/dashboard"],
69058
69298
  "prerequisites": []
@@ -69082,6 +69322,46 @@ Each entry is exactly one of:
69082
69322
  3 = medium (navigation, secondary features)
69083
69323
  4 = low (edge cases, cosmetic)
69084
69324
  5 = nice-to-have`;
69325
+ exports.PLANNER_STRENGTHEN_GATE_PROMPT = `You are a senior QA lead performing ONE design-audit pass over a test PLAN another model wrote for a single web feature. The plan will be handed to a LIVE generator that opens the real app, follows each scenario's steps, observes the DOM, and writes the concrete assertions itself. Division of labor you MUST respect:
69326
+ - The PLAN owns the ORACLE: WHAT must be true for the behavior to be correct, stated in plain English.
69327
+ - The GENERATOR owns BINDING: locators and the exact runtime values it reads off the live page.
69328
+ Never write an expectation whose truth is decided by looking at the current app ("whatever the page shows"): that lets a live bug become the expected result. State the intended truth; the generator will bind it and report a mismatch if reality disagrees.
69329
+
69330
+ You are given (1) the EXACT evidence the plan was based on \u2014 the ONLY ground truth; you may use it to know WHAT behaviors exist, but you must NEVER introduce a behavior, route, element, or effect not present in or directly implied by it \u2014 and (2) the PLAN as JSON. Return the complete corrected plan.
69331
+
69332
+ ## Rule 0 \u2014 REPAIR OR DROP, never merely describe (highest priority)
69333
+ Do NOT write a reasoning note about a fix you did not actually apply. Every change you claim must be present in the scenarios you output: if you say you strengthened an outcome, the new outcome text must be there; if you say you merged or removed a duplicate, NO surviving scenario may share the same primary action + outcome. If a defect cannot be repaired from the evidence, DROP the scenario or FLAG it \u2014 do not keep a known-weak scenario with an excuse. Flag-and-keep is forbidden.
69334
+
69335
+ Your only legal moves per scenario are: KEEP unchanged \xB7 REWRITE using a grounded fact \xB7 DROP as unsupported/duplicate \xB7 FLAG as needs-more-evidence (low reliability, honest note). You may never FORCE-REWRITE by inventing an effect the evidence does not show (a visible Delete control does not tell you whether delete removes, archives, hides, or disables \u2014 do not assume).
69336
+
69337
+ ## Rule 1 \u2014 Classify each scenario, then require the matching oracle
69338
+ Identify the scenario's archetype and hold its expectedOutcomes to the right shape. An outcome is only valid if it would be FALSE under a plausible FAULT for that archetype:
69339
+
69340
+ - CREATE/EDIT/DELETE (mutation): the scenario must control its own target \u2014 create the entity it acts on with a self-chosen unique identifier (create \u2192 act \u2192 verify), or declare a prerequisite that resolves it (a fixture or a stable immutable reference). Never depend on unspecified pre-existing/ambient rows ("delete the first row", "open an existing order"). Oracle = the owned entity's before\u2192after: it appears after create / reflects the edit / is absent after delete (verify by the unique identifier, ideally after reload), while an unrelated sentinel is unaffected. Plausible faults to defeat: no-op, wrong-target, duplicate, non-persistence-after-reload, over-broad delete.
69341
+ - LIST/READ/DETAIL: oracle = the returned content corresponds to what was requested (the opened item's identifier matches the one selected). No before\u2192after is required.
69342
+ - FILTER/SEARCH/SORT: oracle = a delta \u2014 a known in-scope item remains AND a known out-of-scope item disappears (or order changes as specified). "Matching items are shown" alone is a no-op (a broken filter that shows everything passes).
69343
+ - VALIDATION/NEGATIVE: oracle must distinguish "the action was correctly rejected by validation" from "nothing happened / a dead control". Prefer the grounded error signal the evidence recorded (exact message or the specific field marked invalid) plus the absence of the forbidden effect (no entity created). If no error signal was observed, keep it structural at low reliability and FLAG.
69344
+ - NAVIGATION/REDIRECT/AUTH-GUARD: the destination is a legitimate primary effect \u2014 assert the route AND one observed landing signal (heading/content) so a redirect to a broken page fails. Route-presence as an outcome on a NON-navigation scenario is padding \u2014 drop it.
69345
+ - TOGGLE/IDEMPOTENT: assert the changed attribute/state (e.g. pressed/checked/disabled), or for idempotent actions that no duplicate is created \u2014 not mere visibility.
69346
+ - EXTERNAL INTEGRATION: assert the outbound URL/payload contains the expected parameters (drawn from evidence), not a frozen full string.
69347
+ - STATIC CONTENT: presence/label oracle is acceptable; do not inflate it. Verify shared chrome (nav/footer) ONCE, not per page.
69348
+ - APP BUG: exactly one BUG_REPORT per distinct root cause; assert the observed defective effect. Console errors, thrown exceptions, and network status codes are NOT page text \u2014 assert the observable page effect (no toast / no redirect / form unchanged), or set the outcome type to the right probe.
69349
+
69350
+ ## Rule 2 \u2014 Exact values: judge by ORIGIN, not specificity
69351
+ - DROP as brittle: mutable ambient snapshots the test does not control \u2014 current counts ("8 cards"), seeded record names, other users' data, opaque IDs/slugs, phone numbers. Phrase these relationally ("the account's active business name", "the created item", "\u22651 result").
69352
+ - KEEP exact: values the SCENARIO itself introduced (a unique title it typed \u2014 assert it propagates), app-authored static strings (validation messages, headings, template copy, fixed error codes), and stable contract/derived facts (a contractual price, total = qty \xD7 unit price). These are real oracles, not brittleness.
69353
+
69354
+ ## Rule 3 \u2014 Do not build on flows the evidence shows are broken or unrepeatable
69355
+ If evidence flags a route/flow as broken, a precondition as unobtainable (e.g. a single-use token already consumed, an account locked out), or a mutation as irreversible/one-shot on a shared account, you MUST NOT emit happy-path scenarios that depend on that flow succeeding or repeating. Reconcile: drop the dependent happy path, or route it through an alternate path the evidence actually shows.
69356
+
69357
+ ## Rule 4 \u2014 Suite economy and completeness
69358
+ Budget is a ceiling, not a target. Merge scenarios that collide on (route + primary action + outcome-class). Do not enumerate one scenario per item across a long list when 1\u20133 representatives suffice. Reclaim freed budget to cover a DISTINCT behavior the evidence recorded that the plan left untested (swap, do not inflate). An action inferred only from a merely-listed control the evidence never showed exercised should be dropped and FLAGGED as a survey gap, not asserted.
69359
+
69360
+ ## Rule 5 \u2014 reliability is honest
69361
+ high = the effect and its trigger are both directly observed in evidence; medium = observed but partial/indirect; low = structural or inferred (must be FLAGGED). Never launder an unsupported claim by labelling it low \u2014 if it is invented, drop it.
69362
+
69363
+ ## Output
69364
+ Keep the exact JSON schema { "featureGroups":[...], "scenarios":[...], "reasoning":"..." }; each scenario keeps name, type, priority, variant, featureGroup, goal, steps[], expectedOutcomes[{type,description,reliability}], targetPages, prerequisites. In "reasoning", state ONLY changes you actually made (kept/rewritten/dropped/merged/flagged counts). Return ONLY the complete corrected JSON object \u2014 no markdown, no prose outside JSON.`;
69085
69365
  function coerceRawPrerequisites(raw) {
69086
69366
  if (!Array.isArray(raw))
69087
69367
  return [];
@@ -69865,6 +70145,95 @@ Each entry is exactly one of:
69865
70145
  }
69866
70146
  }
69867
70147
  }
70148
+ const gateFlag = (process.env.PLANNER_STRENGTHEN_GATE || "").trim().toLowerCase();
70149
+ const gateEnabled = gateFlag !== "false" && gateFlag !== "0";
70150
+ if (gateEnabled && context.mode !== "survey" && !options.systemPromptOverride && !options.userPromptOverride && !extraPlannerCallUsed && scenarios.length > 0) {
70151
+ const gateModel = options.gateModelOverride ? (0, ai_provider_js_1.resolveModelForTask)(options.gateModelOverride, "agent") : agentModel;
70152
+ const gateClient = options.gateModelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(options.gateModelOverride) : aiClient;
70153
+ const gateMaxTokens = getAIPlannerMaxTokens(gateModel);
70154
+ const gatePlan = { featureGroups, scenarios, reasoning: aiOutput.reasoning ?? "" };
70155
+ const gateUser = `EVIDENCE (ground truth):
70156
+ ${userPrompt}
70157
+
70158
+ =====
70159
+
70160
+ PLAN TO AUDIT AND REPAIR (JSON):
70161
+ ${JSON.stringify(gatePlan)}`;
70162
+ const gateStart = Date.now();
70163
+ let gateResponse = null;
70164
+ try {
70165
+ extraPlannerCallUsed = true;
70166
+ gateResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
70167
+ arm();
70168
+ return gateClient.chat.completions.create({
70169
+ model: gateModel,
70170
+ messages: [
70171
+ { role: "system", content: exports.PLANNER_STRENGTHEN_GATE_PROMPT },
70172
+ { role: "user", content: gateUser }
70173
+ ],
70174
+ max_tokens: gateMaxTokens,
70175
+ temperature: 0.2,
70176
+ ...(0, ai_provider_js_1.getJsonObjectResponseFormat)(gateModel),
70177
+ ...(0, ai_provider_js_1.getNoThinkingRequestOptions)(gateModel)
70178
+ }, { signal });
70179
+ }), {
70180
+ label: "ai-planner-strengthen-gate",
70181
+ deferTimeoutUntilArmed: true,
70182
+ onRetry: (attempt, reason, delay) => logs2.push(`AI planner strengthen-gate call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
70183
+ });
70184
+ } catch (err) {
70185
+ const message = err instanceof Error ? err.message : String(err);
70186
+ logs2.push(`AI planner strengthen-gate failed (${message}) \u2014 keeping pre-gate result`);
70187
+ }
70188
+ if (gateResponse) {
70189
+ try {
70190
+ options.onAICall?.({
70191
+ iteration: 1,
70192
+ groupId: `ai-planner-${context.projectId.slice(0, 8)}`,
70193
+ callType: "discoveryAIPlannerStrengthenGate",
70194
+ model: gateModel,
70195
+ provider: (0, ai_provider_js_1.getProviderForModel)(gateModel),
70196
+ durationMs: Date.now() - gateStart,
70197
+ promptTokens: gateResponse.usage?.prompt_tokens,
70198
+ completionTokens: gateResponse.usage?.completion_tokens,
70199
+ totalTokens: gateResponse.usage?.total_tokens,
70200
+ assistantMessage: gateResponse.choices[0]?.message?.content,
70201
+ requestMessageCount: 2
70202
+ });
70203
+ } catch {
70204
+ }
70205
+ const gateRaw = gateResponse.choices[0]?.message?.content?.trim();
70206
+ if (gateRaw) {
70207
+ let gateOut = null;
70208
+ try {
70209
+ gateOut = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(gateRaw));
70210
+ } catch (err) {
70211
+ const message = err instanceof Error ? err.message : String(err);
70212
+ logs2.push(`AI planner strengthen-gate returned invalid JSON (${message}) \u2014 keeping pre-gate result`);
70213
+ }
70214
+ if (gateOut) {
70215
+ try {
70216
+ const vg = validateAIPlannerOutput(gateOut, siteMap, context.existingTestNames, context.testBudget, evidence.recordedJourneys, context.mode, context.focusAreas);
70217
+ const preGateCount = scenarios.length;
70218
+ const keepsCoverage = vg.scenarios.length >= Math.ceil(preGateCount * 0.5);
70219
+ const noWorseGaps = vg.coverageGaps.length <= coverageGaps.length;
70220
+ if (vg.scenarios.length > 0 && keepsCoverage && noWorseGaps) {
70221
+ logs2.push(`AI planner strengthen-gate [${gateModel}]: ${preGateCount}\u2192${vg.scenarios.length} scenarios, gaps ${coverageGaps.length}\u2192${vg.coverageGaps.length}; taking gated result`);
70222
+ scenarios = vg.scenarios;
70223
+ featureGroups = vg.featureGroups;
70224
+ warnings = [...warnings, ...vg.warnings];
70225
+ coverageGaps = vg.coverageGaps;
70226
+ } else {
70227
+ logs2.push(`AI planner strengthen-gate rejected (scen ${preGateCount}\u2192${vg.scenarios.length}, gaps ${coverageGaps.length}\u2192${vg.coverageGaps.length}); keeping pre-gate result`);
70228
+ }
70229
+ } catch (err) {
70230
+ const message = err instanceof Error ? err.message : String(err);
70231
+ logs2.push(`AI planner strengthen-gate validation threw (${message}) \u2014 keeping pre-gate result`);
70232
+ }
70233
+ }
70234
+ }
70235
+ }
70236
+ }
69868
70237
  const totalFinal = scenarios.length + coverageGaps.length;
69869
70238
  const rejectionRateFinal = totalFinal > 0 ? coverageGaps.length / totalFinal : 0;
69870
70239
  if (rejectionRateFinal > 0.5 && totalFinal >= 3) {
@@ -73644,7 +74013,7 @@ var require_mobile_heal_postprocess = __commonJS({
73644
74013
  // OpenAI-style secret key
73645
74014
  ];
73646
74015
  var EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
73647
- function escapeRegExp(value) {
74016
+ function escapeRegExp2(value) {
73648
74017
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
73649
74018
  }
73650
74019
  function collectSecretVars(testVariables, minLen, keyRe) {
@@ -73663,7 +74032,7 @@ var require_mobile_heal_postprocess = __commonJS({
73663
74032
  return text;
73664
74033
  let out = text;
73665
74034
  for (const { key, value } of collectSecretVars(testVariables, MIN_REDACT_LEN, REDACT_KEY_RE)) {
73666
- out = out.replace(new RegExp(escapeRegExp(value), "g"), `{{${key}}}`);
74035
+ out = out.replace(new RegExp(escapeRegExp2(value), "g"), `{{${key}}}`);
73667
74036
  }
73668
74037
  for (const source of TOKEN_SOURCES) {
73669
74038
  out = out.replace(new RegExp(source, "g"), "[REDACTED]");
@@ -76063,7 +76432,7 @@ var require_api_auth_headers = __commonJS({
76063
76432
  "../runner-core/dist/services/api-auth-headers.js"(exports) {
76064
76433
  "use strict";
76065
76434
  Object.defineProperty(exports, "__esModule", { value: true });
76066
- exports.parseApiAuthHeaders = parseApiAuthHeaders;
76435
+ exports.parseApiAuthHeaders = parseApiAuthHeaders2;
76067
76436
  exports.resolveApiAuthHeaders = resolveApiAuthHeaders;
76068
76437
  exports.isAppOriginRequest = isAppOriginRequest;
76069
76438
  exports.buildOriginScopedApiFixtures = buildOriginScopedApiFixtures;
@@ -76076,7 +76445,7 @@ var require_api_auth_headers = __commonJS({
76076
76445
  }
76077
76446
  return false;
76078
76447
  }
76079
- function parseApiAuthHeaders(raw) {
76448
+ function parseApiAuthHeaders2(raw) {
76080
76449
  if (!raw || typeof raw !== "string")
76081
76450
  return void 0;
76082
76451
  const trimmed = raw.trim();
@@ -76100,9 +76469,9 @@ var require_api_auth_headers = __commonJS({
76100
76469
  function resolveApiAuthHeaders(input) {
76101
76470
  if (input.explicit)
76102
76471
  return input.explicit;
76103
- if (!input.isApiTest)
76472
+ if (input.standingHeadersMode === "none")
76104
76473
  return void 0;
76105
- return parseApiAuthHeaders(input.apiAuthHeadersVar);
76474
+ return parseApiAuthHeaders2(input.apiAuthHeadersVar);
76106
76475
  }
76107
76476
  function isAppOriginRequest(url, baseUrl) {
76108
76477
  if (typeof url !== "string" || !baseUrl)
@@ -76170,6 +76539,126 @@ export const test = base.extend({
76170
76539
  }
76171
76540
  });
76172
76541
 
76542
+ // ../runner-core/dist/services/run-callback-proxy.js
76543
+ var require_run_callback_proxy = __commonJS({
76544
+ "../runner-core/dist/services/run-callback-proxy.js"(exports) {
76545
+ "use strict";
76546
+ Object.defineProperty(exports, "__esModule", { value: true });
76547
+ exports.startRunCallbackProxy = startRunCallbackProxy;
76548
+ var node_crypto_1 = __require("crypto");
76549
+ var node_http_1 = __require("http");
76550
+ var MAX_CALLBACK_BODY_BYTES = 5 * 1024 * 1024;
76551
+ var CALLBACK_TIMEOUT_MS = 3e4;
76552
+ function sendJson(res, status, body) {
76553
+ const payload = JSON.stringify(body);
76554
+ res.writeHead(status, {
76555
+ "cache-control": "no-store",
76556
+ "content-type": "application/json",
76557
+ "content-length": Buffer.byteLength(payload)
76558
+ });
76559
+ res.end(payload);
76560
+ }
76561
+ async function readRequestBody(req) {
76562
+ const chunks = [];
76563
+ let total = 0;
76564
+ for await (const chunk of req) {
76565
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
76566
+ total += buffer.length;
76567
+ if (total > MAX_CALLBACK_BODY_BYTES)
76568
+ throw new Error("callback body too large");
76569
+ chunks.push(buffer);
76570
+ }
76571
+ return Buffer.concat(chunks);
76572
+ }
76573
+ async function startRunCallbackProxy(input) {
76574
+ const upstream = new URL(input.serverUrl);
76575
+ if (upstream.protocol !== "http:" && upstream.protocol !== "https:") {
76576
+ throw new Error("callback server URL must use http or https");
76577
+ }
76578
+ const runPath = `/api/runner/runs/${encodeURIComponent(input.runId)}`;
76579
+ const allowedPaths = /* @__PURE__ */ new Set([`${runPath}/credential`, `${runPath}/auth-state`]);
76580
+ const token = (0, node_crypto_1.randomBytes)(32).toString("base64url");
76581
+ const authHeader = `Bearer ${token}`;
76582
+ const server3 = (0, node_http_1.createServer)(async (req, res) => {
76583
+ try {
76584
+ const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
76585
+ if (req.method !== "POST" || !allowedPaths.has(requestUrl.pathname) || requestUrl.search) {
76586
+ sendJson(res, 404, { error: "Callback route not available" });
76587
+ return;
76588
+ }
76589
+ if (req.headers.authorization !== authHeader) {
76590
+ sendJson(res, 401, { error: "Invalid callback capability" });
76591
+ return;
76592
+ }
76593
+ const body = await readRequestBody(req);
76594
+ let parsedBody;
76595
+ try {
76596
+ const parsed = JSON.parse(body.toString("utf8"));
76597
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
76598
+ throw new Error("invalid body");
76599
+ parsedBody = parsed;
76600
+ } catch {
76601
+ sendJson(res, 400, { error: "Callback body must be a JSON object" });
76602
+ return;
76603
+ }
76604
+ const upstreamBody = JSON.stringify({
76605
+ ...parsedBody,
76606
+ ...input.taskArn ? { taskArn: input.taskArn } : {},
76607
+ ...input.claimEpoch !== void 0 ? { claimEpoch: input.claimEpoch } : {},
76608
+ ...input.authConfigHash ? { authConfigHash: input.authConfigHash } : {}
76609
+ });
76610
+ const destination = new URL(requestUrl.pathname, upstream);
76611
+ const response = await fetch(destination, {
76612
+ method: "POST",
76613
+ headers: {
76614
+ Authorization: input.upstreamAuthHeader,
76615
+ "Content-Type": "application/json"
76616
+ },
76617
+ body: upstreamBody,
76618
+ redirect: "error",
76619
+ signal: AbortSignal.timeout(CALLBACK_TIMEOUT_MS)
76620
+ });
76621
+ const responseBody = Buffer.from(await response.arrayBuffer());
76622
+ res.writeHead(response.status, {
76623
+ "cache-control": "no-store",
76624
+ "content-type": response.headers.get("content-type") ?? "application/json",
76625
+ "content-length": responseBody.length
76626
+ });
76627
+ res.end(responseBody);
76628
+ } catch (error2) {
76629
+ const message = error2 instanceof Error && error2.message === "callback body too large" ? error2.message : "Callback forwarding failed";
76630
+ sendJson(res, message === "callback body too large" ? 413 : 502, { error: message });
76631
+ }
76632
+ });
76633
+ server3.on("clientError", (_error, socket) => socket.destroy());
76634
+ await new Promise((resolve2, reject) => {
76635
+ const onError = (error2) => reject(error2);
76636
+ server3.once("error", onError);
76637
+ server3.listen(0, "127.0.0.1", () => {
76638
+ server3.off("error", onError);
76639
+ resolve2();
76640
+ });
76641
+ });
76642
+ const address = server3.address();
76643
+ if (!address) {
76644
+ server3.close();
76645
+ throw new Error("callback proxy failed to bind");
76646
+ }
76647
+ let closed = false;
76648
+ return {
76649
+ serverUrl: `http://127.0.0.1:${address.port}`,
76650
+ authHeader,
76651
+ close: async () => {
76652
+ if (closed)
76653
+ return;
76654
+ closed = true;
76655
+ await new Promise((resolve2) => server3.close(() => resolve2()));
76656
+ }
76657
+ };
76658
+ }
76659
+ }
76660
+ });
76661
+
76173
76662
  // ../runner-core/dist/services/playwright-native.js
76174
76663
  var require_playwright_native = __commonJS({
76175
76664
  "../runner-core/dist/services/playwright-native.js"(exports) {
@@ -76214,6 +76703,7 @@ var require_playwright_native = __commonJS({
76214
76703
  Object.defineProperty(exports, "__esModule", { value: true });
76215
76704
  exports.scrubHarRequestBody = exports.buildOriginScopedApiFixtures = exports.isAppOriginRequest = exports.resolveApiAuthHeaders = exports.parseApiAuthHeaders = void 0;
76216
76705
  exports.executeTestViaNativePlaywright = executeTestViaNativePlaywright2;
76706
+ exports.buildNativePlaywrightChildEnv = buildNativePlaywrightChildEnv;
76217
76707
  exports.parseJsonResults = parseJsonResults2;
76218
76708
  exports.parseHarToApiSummary = parseHarToApiSummary;
76219
76709
  var promises_1 = __require("fs/promises");
@@ -76225,6 +76715,7 @@ var require_playwright_native = __commonJS({
76225
76715
  var api_call_redaction_js_1 = require_api_call_redaction();
76226
76716
  var har_stream_js_1 = require_har_stream();
76227
76717
  var api_auth_headers_js_1 = require_api_auth_headers();
76718
+ var run_callback_proxy_js_1 = require_run_callback_proxy();
76228
76719
  var api_auth_headers_js_2 = require_api_auth_headers();
76229
76720
  Object.defineProperty(exports, "parseApiAuthHeaders", { enumerable: true, get: function() {
76230
76721
  return api_auth_headers_js_2.parseApiAuthHeaders;
@@ -76247,7 +76738,7 @@ var require_playwright_native = __commonJS({
76247
76738
  var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
76248
76739
  var PW_CLI = __require.resolve("@playwright/test/cli");
76249
76740
  async function executeTestViaNativePlaywright2(options) {
76250
- const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
76741
+ const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, authStandingMode, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
76251
76742
  const logs2 = [];
76252
76743
  const screenshots = [];
76253
76744
  logs2.push(`\u{1F3AD} Native Playwright starting`);
@@ -76257,15 +76748,19 @@ var require_playwright_native = __commonJS({
76257
76748
  logs2.push("\u26A0\uFE0F WARNING: baseUrl is empty - test will likely fail with navigation errors");
76258
76749
  }
76259
76750
  let runDir = null;
76751
+ let standingNeedleMatcher = null;
76752
+ let callbackProxy;
76260
76753
  try {
76261
76754
  runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
76262
76755
  await (0, promises_1.mkdir)(runDir, { recursive: true });
76263
76756
  const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
76757
+ const effectiveStandingMode = isApiTest2 ? authStandingMode ?? "legacy-if-configured" : "none";
76264
76758
  const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
76265
76759
  explicit: extraHttpHeaders,
76266
- isApiTest: isApiTest2,
76760
+ standingHeadersMode: effectiveStandingMode,
76267
76761
  apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
76268
76762
  });
76763
+ standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)(resolvedExtraHeaders ? Object.values(resolvedExtraHeaders) : []);
76269
76764
  const cssDisableSetup = `
76270
76765
  const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
76271
76766
  let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
@@ -76460,6 +76955,18 @@ test.afterEach(async ({ page }, testInfo) => {
76460
76955
  logs2.push("\u26A0 Could not find @playwright/test import - animation disabling NOT injected");
76461
76956
  }
76462
76957
  const useScopedApiHeaders = isApiTest2 && !!resolvedExtraHeaders && !!importMatch && !!baseUrl;
76958
+ const authDelivery = { standingHeadersConfigured: !!resolvedExtraHeaders, standingHeadersInjected: useScopedApiHeaders };
76959
+ if (resolvedExtraHeaders && !useScopedApiHeaders) {
76960
+ logs2.push("\u26A0 Standing API auth headers were configured but could not be installed origin-scoped \u2014 failing closed (no global fallback).");
76961
+ return {
76962
+ passed: false,
76963
+ stepResults: [],
76964
+ screenshots: [],
76965
+ logs: logs2.join("\n"),
76966
+ error: 'CONFIG_ISSUE: Standing API auth headers were configured but could not be installed origin-scoped (needs a rewritable `import { test } from "@playwright/test"` and BASE_URL); refusing to attach them globally to avoid an off-origin credential leak.',
76967
+ authDelivery
76968
+ };
76969
+ }
76463
76970
  let instrumentedCode;
76464
76971
  if (isApiTest2) {
76465
76972
  instrumentedCode = useScopedApiHeaders ? playwrightCode.replace(importMatch[0], importMatch[0].replace(/from\s+['"]@playwright\/test['"]/, "from './qa-api-fixtures'")) : playwrightCode;
@@ -76483,7 +76990,7 @@ test.afterEach(async ({ page }, testInfo) => {
76483
76990
  const absHarPath = wantHar ? (0, path_1.join)(runDir, "session.har").replace(/\\/g, "/") : null;
76484
76991
  const recordHarLine = absHarPath ? `recordHar: { path: ${JSON.stringify(absHarPath)}, mode: 'full' },` : "";
76485
76992
  const storageStateLine = storageStatePath ? `storageState: ${JSON.stringify(storageStatePath.replace(/\\/g, "/"))},` : "";
76486
- const extraHeadersLine = resolvedExtraHeaders && !useScopedApiHeaders && Object.keys(resolvedExtraHeaders).length > 0 ? `extraHTTPHeaders: ${JSON.stringify(resolvedExtraHeaders)},` : "";
76993
+ const extraHeadersLine = "";
76487
76994
  const launchArgs = IS_CLOUD2 ? "args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', '--no-zygote', '--disable-accelerated-2d-canvas', '--js-flags=--max-old-space-size=512']," : "args: ['--disable-blink-features=AutomationControlled'],";
76488
76995
  const config = `
76489
76996
  // Plain object export - no require('@playwright/test') needed.
@@ -76516,35 +77023,51 @@ module.exports = {
76516
77023
  const configPath = (0, path_1.join)(runDir, "playwright.config.js");
76517
77024
  await (0, promises_1.writeFile)(configPath, config);
76518
77025
  const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(runDir);
76519
- const envWithIsolation = {
76520
- ...testVariables ?? {},
77026
+ const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
77027
+ const callbackClaimEpoch = Number.isSafeInteger(validateContext?.claimEpoch) ? validateContext?.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
77028
+ if (validateContext?.runId && validateContext.serverUrl && validateContext.authHeader) {
77029
+ try {
77030
+ callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
77031
+ runId: validateContext.runId,
77032
+ serverUrl: validateContext.serverUrl,
77033
+ upstreamAuthHeader: validateContext.authHeader,
77034
+ taskArn: process.env.VALIDATE_TASK_ARN,
77035
+ claimEpoch: callbackClaimEpoch,
77036
+ authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
77037
+ });
77038
+ } catch (error3) {
77039
+ logs2.push(`Callback proxy unavailable: ${error3 instanceof Error ? error3.message : String(error3)}`);
77040
+ }
77041
+ }
77042
+ const childTestVariables = { ...testVariables ?? {} };
77043
+ const trustedRuntimeEnv = {
76521
77044
  ...isolation.env,
76522
- // persistCredential() callback envs — set only when the runner supplied
76523
- // validateContext. Without these the helper inside the test no-ops with
76524
- // a warning, which is the correct behavior when the test is being run
76525
- // manually (outside the validate.qa harness).
76526
- ...validateContext?.runId ? { VALIDATE_RUN_ID: validateContext.runId } : {},
76527
- ...validateContext?.serverUrl ? { VALIDATE_SERVER_URL: validateContext.serverUrl } : {},
76528
- ...validateContext?.authHeader ? { VALIDATE_RUNNER_AUTH: validateContext.authHeader } : {},
77045
+ // Generated callbacks receive only a random loopback capability. The global
77046
+ // runner bearer token and real control-plane URL stay in the parent process.
77047
+ ...callbackProxy && validateContext?.runId ? {
77048
+ VALIDATE_RUN_ID: validateContext.runId,
77049
+ ...callbackClaimEpoch !== void 0 ? { VALIDATE_CLAIM_EPOCH: String(callbackClaimEpoch) } : {},
77050
+ VALIDATE_SERVER_URL: callbackProxy.serverUrl,
77051
+ VALIDATE_RUNNER_AUTH: callbackProxy.authHeader
77052
+ } : {},
76529
77053
  // Owning Fargate taskArn (a per-task constant set once at task boot in
76530
77054
  // execute-one.ts; absent on the local/Railway runner). Read from
76531
77055
  // process.env rather than validateContext since it's process-wide, not
76532
77056
  // per-run. Lets persistCredential() prove ownership to /credential.
76533
77057
  ...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {},
77058
+ ...authStateCapturePath ? { AUTH_FILE: authStateCapturePath } : {},
76534
77059
  ...liveBrowserStatePath ? { VALIDATE_LIVE_BROWSER_STATE_PATH: liveBrowserStatePath } : {},
76535
- // Shared-auth kill-switch (SHARED_AUTH_INJECT) must reach the child: the stamped
76536
- // __vqaLogin's early-publish POST is gated on it at RUNTIME. buildSandboxedEnv only
76537
- // copies an allowlist from process.env (SHARED_AUTH_INJECT is not on it), so without
76538
- // this forward the flag reads as undefined in the test and the early publish is dead
76539
- // code. Forwarded (not allowlisted) so it stays a per-run toggle; absent → inert.
77060
+ // Preserve an explicit shared-auth kill-switch in the child. Unset defaults on in
77061
+ // both parent and generated __vqaLogin code; "false" must still cross this process
77062
+ // boundary to disable the early publish POST. It remains a per-run toggle.
76540
77063
  ...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
76541
77064
  };
76542
- delete envWithIsolation.API_AUTH_HEADERS;
77065
+ delete childTestVariables.API_AUTH_HEADERS;
76543
77066
  logs2.push("\u{1F680} Spawning playwright test...");
76544
77067
  const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
76545
77068
  let playResult;
76546
77069
  try {
76547
- playResult = await runPlaywright(configPath, envWithIsolation);
77070
+ playResult = await runPlaywright(configPath, childTestVariables, trustedRuntimeEnv);
76548
77071
  } finally {
76549
77072
  await stopLiveBrowserPolling?.();
76550
77073
  }
@@ -76605,7 +77128,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76605
77128
  logs2.push(`\u26A0 HAR parse skipped: ${msg.slice(0, 200)}`);
76606
77129
  }
76607
77130
  }
76608
- return {
77131
+ const nativeResult = {
76609
77132
  passed,
76610
77133
  stepResults,
76611
77134
  screenshots,
@@ -76614,20 +77137,27 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76614
77137
  harApiCalls,
76615
77138
  logs: logs2.join("\n"),
76616
77139
  error: passed ? void 0 : error2,
76617
- suspiciousPass
77140
+ suspiciousPass,
77141
+ authDelivery
76618
77142
  };
77143
+ if (!standingNeedleMatcher)
77144
+ return nativeResult;
77145
+ return (0, shared_1.redactNeedlesDeep)(nativeResult, standingNeedleMatcher);
76619
77146
  } catch (fatalErr) {
76620
77147
  const msg = fatalErr instanceof Error ? fatalErr.message : String(fatalErr);
76621
77148
  logs2.push(`
76622
77149
  \u{1F4A5} Fatal error: ${msg}`);
76623
- return {
77150
+ const fatalResult = {
76624
77151
  passed: false,
76625
77152
  stepResults: [],
76626
77153
  screenshots,
76627
77154
  logs: logs2.join("\n"),
76628
77155
  error: msg
76629
77156
  };
77157
+ return standingNeedleMatcher ? (0, shared_1.redactNeedlesDeep)(fatalResult, standingNeedleMatcher) : fatalResult;
76630
77158
  } finally {
77159
+ await callbackProxy?.close().catch(() => {
77160
+ });
76631
77161
  if (runDir) {
76632
77162
  await (0, promises_1.rm)(runDir, { recursive: true, force: true }).catch(() => {
76633
77163
  });
@@ -76679,9 +77209,9 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76679
77209
  stopped = true;
76680
77210
  };
76681
77211
  }
76682
- function runPlaywright(configPath, extraEnv) {
77212
+ function runPlaywright(configPath, testVariables, trustedRuntimeEnv) {
76683
77213
  return new Promise((resolvePromise) => {
76684
- const sandboxedEnv = (0, shared_1.buildSandboxedEnv)(process.env, extraEnv);
77214
+ const sandboxedEnv = buildNativePlaywrightChildEnv(process.env, testVariables, trustedRuntimeEnv);
76685
77215
  const proc = (0, spawn_with_tree_kill_js_1.spawnDetachedGroup)(process.execPath, [PW_CLI, "test", "--config", configPath], {
76686
77216
  cwd: PACKAGE_DIR,
76687
77217
  env: sandboxedEnv,
@@ -76724,6 +77254,12 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76724
77254
  });
76725
77255
  });
76726
77256
  }
77257
+ function buildNativePlaywrightChildEnv(processEnv, testVariables, trustedRuntimeEnv) {
77258
+ return {
77259
+ ...(0, shared_1.buildSandboxedEnv)(processEnv, testVariables),
77260
+ ...trustedRuntimeEnv ?? {}
77261
+ };
77262
+ }
76727
77263
  async function collectScreenshots(dir, screenshots) {
76728
77264
  let entries;
76729
77265
  try {
@@ -80445,10 +80981,12 @@ var require_auth_recipe = __commonJS({
80445
80981
  exports.upsertAuthBlock = upsertAuthBlock;
80446
80982
  exports.buildHealAuthStamp = buildHealAuthStamp;
80447
80983
  var crypto_1 = __require("crypto");
80984
+ var shared_1 = (init_dist(), __toCommonJS(dist_exports));
80448
80985
  var discovery_lint_flags_js_1 = require_discovery_lint_flags();
80449
- exports.AUTH_MARKER_VERSION = "v1";
80986
+ exports.AUTH_MARKER_VERSION = "v3";
80450
80987
  var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
80451
80988
  var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} <<<`;
80989
+ var SHARED_AUTH_RUNTIME_ENABLED = `!/^false$/i.test((process.env.SHARED_AUTH_INJECT ?? '').trim())`;
80452
80990
  var AUTH_REGION_RE = /\/\/ >>> VALIDATE_QA_AUTH[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH[^\n]*\r?\n?/g;
80453
80991
  function computeRecipeHash(input) {
80454
80992
  const canonical = JSON.stringify({
@@ -80526,16 +81064,18 @@ var require_auth_recipe = __commonJS({
80526
81064
  if (opts?.warmGuard) {
80527
81065
  const warmLanding = recipe.redirectRoute ?? (recipe.authProof.kind === "url" ? recipe.authProof.route : recipe.preNavRoute);
80528
81066
  const loginRoute = recipe.route ?? recipe.preNavRoute;
80529
- lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
81067
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED}) {`);
81068
+ lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
80530
81069
  if (recipe.authProof.kind === "url") {
80531
81070
  const pred = urlProofPredicateExpr(recipe.authProof.route);
80532
- lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
80533
- lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
81071
+ lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
81072
+ lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
80534
81073
  } else if (loginRoute) {
80535
- lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
81074
+ lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
80536
81075
  } else {
80537
- lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
81076
+ lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
80538
81077
  }
81078
+ lines.push(` }`);
80539
81079
  }
80540
81080
  lines.push(` await page.goto(${JSON.stringify(recipe.preNavRoute)}, { waitUntil: 'domcontentloaded' });`);
80541
81081
  for (const cmd of recipe.preNavCmds)
@@ -80548,11 +81088,11 @@ var require_auth_recipe = __commonJS({
80548
81088
  lines.push(renderAuthUrlWaitLine(recipe.redirectRoute));
80549
81089
  lines.push(` ${renderAuthProofAssertion(recipe.authProof)}`);
80550
81090
  if (opts?.warmGuard) {
80551
- lines.push(` if (process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
80552
- lines.push(` if (process.env.SHARED_AUTH_INJECT === 'true' && process.env.VALIDATE_TASK_ARN && process.env.VALIDATE_SERVER_URL && process.env.VALIDATE_RUN_ID) {`);
81091
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
81092
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.VALIDATE_TASK_ARN && process.env.VALIDATE_CLAIM_EPOCH && process.env.VALIDATE_SERVER_URL && process.env.VALIDATE_RUN_ID) {`);
80553
81093
  lines.push(` try {`);
80554
81094
  lines.push(` const __vqaState = await page.context().storageState();`);
80555
- lines.push(` await page.request.post(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + process.env.VALIDATE_RUN_ID + '/auth-state', { headers: { Authorization: process.env.VALIDATE_RUNNER_AUTH || '' }, data: { storageState: JSON.stringify(__vqaState), taskArn: process.env.VALIDATE_TASK_ARN }, timeout: 10000 });`);
81095
+ lines.push(` await page.request.post(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + process.env.VALIDATE_RUN_ID + '/auth-state', { headers: { Authorization: process.env.VALIDATE_RUNNER_AUTH || '' }, data: { storageState: JSON.stringify(__vqaState), taskArn: process.env.VALIDATE_TASK_ARN, claimEpoch: Number(process.env.VALIDATE_CLAIM_EPOCH) }, timeout: 10000 });`);
80556
81096
  lines.push(` } catch {}`);
80557
81097
  lines.push(` }`);
80558
81098
  }
@@ -80564,6 +81104,8 @@ var require_auth_recipe = __commonJS({
80564
81104
  "AUTH_FILE",
80565
81105
  "SHARED_AUTH_INJECT",
80566
81106
  "VALIDATE_RUN_ID",
81107
+ "VALIDATE_CLAIM_EPOCH",
81108
+ "VALIDATE_AUTH_CONFIG_HASH",
80567
81109
  "VALIDATE_TASK_ARN",
80568
81110
  "VALIDATE_RUNNER_AUTH",
80569
81111
  "VALIDATE_SERVER_URL"
@@ -80736,7 +81278,7 @@ ${tail}`;
80736
81278
  let result = stripAuthRegions(code);
80737
81279
  result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
80738
81280
  result = ensureExpectImported(result);
80739
- const warmGuard = process.env.SHARED_AUTH_INJECT === "true";
81281
+ const warmGuard = (0, shared_1.isSharedAuthInjectEnabled)();
80740
81282
  result = insertAfterPwImport(result, renderAuthFn(ctx.recipe, { warmGuard }));
80741
81283
  result = ensureLoginCall(result);
80742
81284
  const n = countAuthRegions(result);
@@ -85716,7 +86258,7 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
85716
86258
  }
85717
86259
  return result;
85718
86260
  }
85719
- function escapeRegExp(value) {
86261
+ function escapeRegExp2(value) {
85720
86262
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
85721
86263
  }
85722
86264
  function buildSensitiveEnvReplacements(testVariables) {
@@ -85738,7 +86280,7 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
85738
86280
  for (const replacement of buildSensitiveEnvReplacements(testVariables)) {
85739
86281
  if (replacement.value.includes("\n") || replacement.value.includes("\r"))
85740
86282
  continue;
85741
- const literalPattern = new RegExp(`(['"\`])${escapeRegExp(replacement.value)}\\1`, "g");
86283
+ const literalPattern = new RegExp(`(['"\`])${escapeRegExp2(replacement.value)}\\1`, "g");
85742
86284
  result = result.replace(literalPattern, replacement.envRef);
85743
86285
  }
85744
86286
  return result;
@@ -85866,9 +86408,9 @@ ${testOpen}`;
85866
86408
  basePath: options?.basePath
85867
86409
  });
85868
86410
  const leakedSensitiveKeys = findSensitiveLiteralLeaks(playwrightCode, options?.sensitiveEnvMap);
85869
- const authGuardStamped = !!options?.authRecipe && rawTags.includes("@needs-auth") && !rawTags.includes("@auth") && process.env.SHARED_AUTH_INJECT === "true";
85870
- const baseTags = ensureDiscoveredTag(Array.isArray(test.tags) ? test.tags : ["@discovered", "@regression"]);
85871
- const tags = authGuardStamped && !baseTags.includes("@auth-guarded") ? [...baseTags, "@auth-guarded"] : baseTags;
86411
+ const authGuardStamped = !!options?.authRecipe && rawTags.includes("@needs-auth") && !rawTags.includes("@auth") && (0, shared_1.isSharedAuthInjectEnabled)() && /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
86412
+ const baseTags = ensureDiscoveredTag(Array.isArray(test.tags) ? test.tags : ["@discovered", "@regression"]).filter((tag) => tag !== "@auth-guarded");
86413
+ const tags = authGuardStamped ? [...baseTags, "@auth-guarded"] : baseTags;
85872
86414
  return {
85873
86415
  name: String(test.name || "Unnamed test").slice(0, 200),
85874
86416
  description: String(test.description || test.name || "").slice(0, 500),
@@ -99683,7 +100225,7 @@ ${lanes.join("\n")}
99683
100225
  return process.memoryUsage().heapUsed;
99684
100226
  },
99685
100227
  getFileSize(path) {
99686
- const stat = statSync2(path);
100228
+ const stat = statSync(path);
99687
100229
  if (stat == null ? void 0 : stat.isFile()) {
99688
100230
  return stat.size;
99689
100231
  }
@@ -99727,7 +100269,7 @@ ${lanes.join("\n")}
99727
100269
  }
99728
100270
  };
99729
100271
  return nodeSystem;
99730
- function statSync2(path) {
100272
+ function statSync(path) {
99731
100273
  try {
99732
100274
  return _fs.statSync(path, statSyncOptions);
99733
100275
  } catch {
@@ -99786,7 +100328,7 @@ ${lanes.join("\n")}
99786
100328
  activeSession.post("Profiler.stop", (err, { profile }) => {
99787
100329
  var _a;
99788
100330
  if (!err) {
99789
- if ((_a = statSync2(profilePath)) == null ? void 0 : _a.isDirectory()) {
100331
+ if ((_a = statSync(profilePath)) == null ? void 0 : _a.isDirectory()) {
99790
100332
  profilePath = _path.join(profilePath, `${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`);
99791
100333
  }
99792
100334
  try {
@@ -99906,7 +100448,7 @@ ${lanes.join("\n")}
99906
100448
  let stat;
99907
100449
  if (typeof dirent === "string" || dirent.isSymbolicLink()) {
99908
100450
  const name = combinePaths(path, entry);
99909
- stat = statSync2(name);
100451
+ stat = statSync(name);
99910
100452
  if (!stat) {
99911
100453
  continue;
99912
100454
  }
@@ -99930,7 +100472,7 @@ ${lanes.join("\n")}
99930
100472
  return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames2, process.cwd(), depth, getAccessibleFileSystemEntries, realpath);
99931
100473
  }
99932
100474
  function fileSystemEntryExists(path, entryKind) {
99933
- const stat = statSync2(path);
100475
+ const stat = statSync(path);
99934
100476
  if (!stat) {
99935
100477
  return false;
99936
100478
  }
@@ -99972,7 +100514,7 @@ ${lanes.join("\n")}
99972
100514
  }
99973
100515
  function getModifiedTime3(path) {
99974
100516
  var _a;
99975
- return (_a = statSync2(path)) == null ? void 0 : _a.mtime;
100517
+ return (_a = statSync(path)) == null ? void 0 : _a.mtime;
99976
100518
  }
99977
100519
  function setModifiedTime(path, time) {
99978
100520
  try {
@@ -254994,7 +255536,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
254994
255536
  }
254995
255537
  }
254996
255538
  return result;
254997
- function escapeRegExp(str) {
255539
+ function escapeRegExp2(str) {
254998
255540
  return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
254999
255541
  }
255000
255542
  function getTodoCommentsRegExp() {
@@ -255002,7 +255544,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
255002
255544
  const multiLineCommentStart = /(?:\/\*+\s*)/.source;
255003
255545
  const anyNumberOfSpacesAndAsterisksAtStartOfLine = /(?:^(?:\s|\*)*)/.source;
255004
255546
  const preamble = "(" + anyNumberOfSpacesAndAsterisksAtStartOfLine + "|" + singleLineCommentStart + "|" + multiLineCommentStart + ")";
255005
- const literals = "(?:" + map2(descriptors, (d) => "(" + escapeRegExp(d.text) + ")").join("|") + ")";
255547
+ const literals = "(?:" + map2(descriptors, (d) => "(" + escapeRegExp2(d.text) + ")").join("|") + ")";
255006
255548
  const endOfLineOrEndOfComment = /(?:$|\*\/)/.source;
255007
255549
  const messageRemainder = /(?:.*?)/.source;
255008
255550
  const messagePortion = "(" + literals + messageRemainder + ")";
@@ -303262,7 +303804,7 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
303262
303804
  confidence: "low"
303263
303805
  };
303264
303806
  }
303265
- function escapeRegExp(s) {
303807
+ function escapeRegExp2(s) {
303266
303808
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
303267
303809
  }
303268
303810
  function tryDeterministicStrictModeFix(code, error2) {
@@ -303285,10 +303827,10 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
303285
303827
  const [, , role, , name, restOfOptions] = violated;
303286
303828
  if (/exact\s*:\s*true/.test(restOfOptions))
303287
303829
  return null;
303288
- const akaPattern = new RegExp(`aka\\s+getByRole\\(\\s*(['"\`])${escapeRegExp(role)}\\1\\s*,\\s*\\{[^}]*?name:\\s*(['"\`])${escapeRegExp(name)}\\2[^}]*?exact:\\s*true[^}]*\\}\\s*\\)`);
303830
+ const akaPattern = new RegExp(`aka\\s+getByRole\\(\\s*(['"\`])${escapeRegExp2(role)}\\1\\s*,\\s*\\{[^}]*?name:\\s*(['"\`])${escapeRegExp2(name)}\\2[^}]*?exact:\\s*true[^}]*\\}\\s*\\)`);
303289
303831
  if (!akaPattern.test(error2))
303290
303832
  return null;
303291
- const codePattern = new RegExp(`(getByRole\\(\\s*(['"\`])${escapeRegExp(role)}\\2\\s*,\\s*\\{[^}]*?name:\\s*(['"\`])${escapeRegExp(name)}\\3)([^}]*\\})`, "g");
303833
+ const codePattern = new RegExp(`(getByRole\\(\\s*(['"\`])${escapeRegExp2(role)}\\2\\s*,\\s*\\{[^}]*?name:\\s*(['"\`])${escapeRegExp2(name)}\\3)([^}]*\\})`, "g");
303292
303834
  let modified = false;
303293
303835
  const patched = code.replace(codePattern, (full, prefix, _q1, _q2, suffix) => {
303294
303836
  if (/exact\s*:\s*true/.test(suffix))
@@ -303305,11 +303847,11 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
303305
303847
  const [, , text, optionsBlock] = violated;
303306
303848
  if (optionsBlock && /exact\s*:\s*true/.test(optionsBlock))
303307
303849
  return null;
303308
- const akaPattern = new RegExp(`aka\\s+getByText\\(\\s*(['"\`])${escapeRegExp(text)}\\1\\s*,\\s*\\{[^}]*?exact:\\s*true[^}]*\\}\\s*\\)`);
303850
+ const akaPattern = new RegExp(`aka\\s+getByText\\(\\s*(['"\`])${escapeRegExp2(text)}\\1\\s*,\\s*\\{[^}]*?exact:\\s*true[^}]*\\}\\s*\\)`);
303309
303851
  if (!akaPattern.test(error2))
303310
303852
  return null;
303311
- const noOptsPattern = new RegExp(`getByText\\(\\s*(['"\`])${escapeRegExp(text)}\\1\\s*\\)`, "g");
303312
- const withOptsPattern = new RegExp(`(getByText\\(\\s*(['"\`])${escapeRegExp(text)}\\2\\s*,\\s*\\{)([^}]*\\})`, "g");
303853
+ const noOptsPattern = new RegExp(`getByText\\(\\s*(['"\`])${escapeRegExp2(text)}\\1\\s*\\)`, "g");
303854
+ const withOptsPattern = new RegExp(`(getByText\\(\\s*(['"\`])${escapeRegExp2(text)}\\2\\s*,\\s*\\{)([^}]*\\})`, "g");
303313
303855
  let modified = false;
303314
303856
  let patched = code.replace(noOptsPattern, (_full, q) => {
303315
303857
  modified = true;
@@ -308483,6 +309025,7 @@ var require_regression_runner = __commonJS({
308483
309025
  var chromium_isolation_js_1 = require_chromium_isolation();
308484
309026
  var browser_config_js_1 = require_browser_config();
308485
309027
  var spawn_with_tree_kill_js_1 = require_spawn_with_tree_kill();
309028
+ var run_callback_proxy_js_1 = require_run_callback_proxy();
308486
309029
  var SUITE_SPAWN_TIMEOUT_MS = 85 * 60 * 1e3;
308487
309030
  var PACKAGE_DIR = (0, path_1.resolve)(__dirname, "..", "..");
308488
309031
  var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
@@ -308497,6 +309040,7 @@ var require_regression_runner = __commonJS({
308497
309040
  const tarPath = (0, path_1.join)(RUNS_DIR, `regression-${runId}.tar.gz`);
308498
309041
  const startTime = Date.now();
308499
309042
  let logs2 = "";
309043
+ let callbackProxy;
308500
309044
  try {
308501
309045
  await (0, promises_1.mkdir)(extractDir, { recursive: true });
308502
309046
  const tarballUrl = `https://api.github.com/repos/${repoOwner}/${repoName}/tarball/${repoBranch}`;
@@ -308526,19 +309070,40 @@ var require_regression_runner = __commonJS({
308526
309070
  await (0, promises_1.writeFile)((0, path_1.join)(extractDir, ".env"), envContent, "utf-8");
308527
309071
  const jsonResultsPath = (0, path_1.join)(extractDir, "test-results", "results.json");
308528
309072
  const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(extractDir);
309073
+ const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
309074
+ const callbackClaimEpoch = Number.isSafeInteger(opts.claimEpoch) ? opts.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
309075
+ if (opts.validateServerUrl && opts.validateRunnerAuthHeader) {
309076
+ try {
309077
+ callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
309078
+ runId,
309079
+ serverUrl: opts.validateServerUrl,
309080
+ upstreamAuthHeader: opts.validateRunnerAuthHeader,
309081
+ taskArn: process.env.VALIDATE_TASK_ARN,
309082
+ claimEpoch: callbackClaimEpoch,
309083
+ authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
309084
+ });
309085
+ } catch (error2) {
309086
+ logs2 += `Callback proxy unavailable: ${error2 instanceof Error ? error2.message : String(error2)}
309087
+ `;
309088
+ }
309089
+ }
308529
309090
  const runEnv = {
308530
309091
  ...(0, shared_1.buildSandboxedEnv)(process.env, testVariables),
308531
309092
  ...isolation.env,
308532
309093
  CI: "true",
308533
309094
  PLAYWRIGHT_JSON_OUTPUT_NAME: jsonResultsPath,
308534
- // Callback envs for tests/setup/credentials.ts → persistCredential.
308535
- // Tests that rotate a credential POST back to the server using these.
308536
- VALIDATE_RUN_ID: runId,
308537
- ...opts.validateServerUrl ? { VALIDATE_SERVER_URL: opts.validateServerUrl } : {},
308538
- ...opts.validateRunnerAuthHeader ? { VALIDATE_RUNNER_AUTH: opts.validateRunnerAuthHeader } : {},
309095
+ // The suite receives a random run-scoped loopback callback, never the
309096
+ // global runner bearer credential.
309097
+ ...callbackProxy ? {
309098
+ VALIDATE_RUN_ID: runId,
309099
+ ...callbackClaimEpoch !== void 0 ? { VALIDATE_CLAIM_EPOCH: String(callbackClaimEpoch) } : {},
309100
+ VALIDATE_SERVER_URL: callbackProxy.serverUrl,
309101
+ VALIDATE_RUNNER_AUTH: callbackProxy.authHeader
309102
+ } : {},
308539
309103
  // Owning Fargate taskArn (per-task constant; unset off-Fargate). Forwarded
308540
309104
  // so persistCredential() can prove ownership to the /credential fence.
308541
- ...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {}
309105
+ ...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {},
309106
+ ...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
308542
309107
  };
308543
309108
  logs2 += `Running playwright --project=${opts.suiteProject ?? "suite"}...
308544
309109
  `;
@@ -308567,6 +309132,8 @@ var require_regression_runner = __commonJS({
308567
309132
  error: String(err)
308568
309133
  };
308569
309134
  } finally {
309135
+ await callbackProxy?.close().catch(() => {
309136
+ });
308570
309137
  (0, promises_1.rm)(extractDir, { recursive: true, force: true }).catch(() => {
308571
309138
  });
308572
309139
  await (0, browser_config_js_1.cleanupPlaywrightTempArtifacts)().catch(() => {
@@ -321241,9 +321808,9 @@ var require_tool_ledger = __commonJS({
321241
321808
  const ledgerPath = (0, node_path_1.join)(runDir, "discovery-events.jsonl");
321242
321809
  let checkpointIndex = 0;
321243
321810
  let headerWritten = false;
321244
- const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
321811
+ const escapeRegExp2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
321245
321812
  const credentialScrubbers = Array.from(credentialEnvMap.entries()).filter(([, value]) => typeof value === "string" && value.length > 0).sort((a, b) => b[1].length - a[1].length).map(([envName, value]) => ({
321246
- re: new RegExp(escapeRegExp(value), "g"),
321813
+ re: new RegExp(escapeRegExp2(value), "g"),
321247
321814
  replacement: `[REDACTED_${envName}]`
321248
321815
  }));
321249
321816
  const scrubCredentials = (text) => {
@@ -323403,6 +323970,7 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
323403
323970
  testBudget
323404
323971
  }, {
323405
323972
  modelOverride: ctx.planModel ?? ctx.generateModel,
323973
+ gateModelOverride: ctx.planGateModel,
323406
323974
  onAICall
323407
323975
  });
323408
323976
  logs2.push(...planResult.logs);
@@ -323981,6 +324549,7 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
323981
324549
  testBudget: featureTestBudget
323982
324550
  }, {
323983
324551
  modelOverride: ctx.planModel ?? ctx.generateModel,
324552
+ gateModelOverride: ctx.planGateModel,
323984
324553
  onAICall
323985
324554
  });
323986
324555
  logs2.push(...planResult.logs);
@@ -324810,6 +325379,455 @@ var require_resource_governor = __commonJS({
324810
325379
  }
324811
325380
  });
324812
325381
 
325382
+ // ../runner-core/dist/services/local-auth-state.js
325383
+ var require_local_auth_state = __commonJS({
325384
+ "../runner-core/dist/services/local-auth-state.js"(exports) {
325385
+ "use strict";
325386
+ Object.defineProperty(exports, "__esModule", { value: true });
325387
+ exports.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS = exports.LOCAL_AUTH_STATE_LOCK_STALE_MS = exports.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS = exports.LOCAL_AUTH_STATE_WAIT_MS = exports.LOCAL_AUTH_STATE_MAX_AGE_MS = void 0;
325388
+ exports.shouldSkipWarmAuthSetup = shouldSkipWarmAuthSetup2;
325389
+ exports.shouldSkipDisabledAutomaticAuthSetup = shouldSkipDisabledAutomaticAuthSetup2;
325390
+ exports.localAuthStateFingerprint = localAuthStateFingerprint;
325391
+ exports.ensurePrivateDirectory = ensurePrivateDirectory2;
325392
+ exports.isValidStorageState = isValidStorageState;
325393
+ exports.isValidStorageStateJson = isValidStorageStateJson;
325394
+ exports.isValidLocalAuthStateFile = isValidLocalAuthStateFile;
325395
+ exports.cleanupLocalAuthStateFiles = cleanupLocalAuthStateFiles;
325396
+ exports.cleanupExpiredLocalAuthStateFiles = cleanupExpiredLocalAuthStateFiles2;
325397
+ exports.localAuthStatePaths = localAuthStatePaths2;
325398
+ exports.getValidLocalAuthState = getValidLocalAuthState2;
325399
+ exports.waitForValidLocalAuthState = waitForValidLocalAuthState;
325400
+ exports.waitForLocalAuthStateProducer = waitForLocalAuthStateProducer2;
325401
+ exports.promoteLocalAuthStateCapture = promoteLocalAuthStateCapture2;
325402
+ exports.discardLocalAuthStateCapture = discardLocalAuthStateCapture2;
325403
+ exports.acquireLocalAuthStateLock = acquireLocalAuthStateLock2;
325404
+ exports.localAuthStateFilename = localAuthStateFilename;
325405
+ var node_fs_1 = __require("fs");
325406
+ var node_crypto_1 = __require("crypto");
325407
+ var node_path_1 = __require("path");
325408
+ var auth_recipe_js_1 = require_auth_recipe();
325409
+ exports.LOCAL_AUTH_STATE_MAX_AGE_MS = 60 * 60 * 1e3;
325410
+ exports.LOCAL_AUTH_STATE_WAIT_MS = 3 * 60 * 1e3;
325411
+ exports.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS = 15 * 60 * 1e3;
325412
+ exports.LOCAL_AUTH_STATE_LOCK_STALE_MS = 2 * 60 * 1e3;
325413
+ exports.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS = 30 * 60 * 1e3;
325414
+ function shouldSkipWarmAuthSetup2(authForceRun, sharedAuthEnabled, cacheWarm) {
325415
+ return sharedAuthEnabled && authForceRun !== true && cacheWarm;
325416
+ }
325417
+ function shouldSkipDisabledAutomaticAuthSetup2(authForceRun, authVerify, sharedAuthEnabled) {
325418
+ return !sharedAuthEnabled && authForceRun !== true && authVerify !== true;
325419
+ }
325420
+ function normalizeOrigin(value) {
325421
+ try {
325422
+ return new URL(value).origin.toLowerCase();
325423
+ } catch {
325424
+ return value.trim().toLowerCase();
325425
+ }
325426
+ }
325427
+ function normalizeAppUrl(value) {
325428
+ try {
325429
+ const url = new URL(value);
325430
+ url.hash = "";
325431
+ return `${url.origin.toLowerCase()}${url.pathname}${url.search}`;
325432
+ } catch {
325433
+ return value.trim();
325434
+ }
325435
+ }
325436
+ function safeSegment(value, fallback) {
325437
+ const safe = value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 120);
325438
+ return safe || fallback;
325439
+ }
325440
+ function stableVariables(testVariables) {
325441
+ return Object.entries(testVariables).filter(([key]) => !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(key)).sort(([a], [b]) => a.localeCompare(b));
325442
+ }
325443
+ function localAuthStateFingerprint(input) {
325444
+ const payload = JSON.stringify({
325445
+ version: 1,
325446
+ projectId: input.projectId,
325447
+ serverOrigin: normalizeOrigin(input.serverUrl),
325448
+ appUrl: normalizeAppUrl(input.baseUrl),
325449
+ testVariables: stableVariables(input.testVariables)
325450
+ });
325451
+ const key = input.authToken || `validate-local-auth:${input.projectId}`;
325452
+ return (0, node_crypto_1.createHmac)("sha256", key).update(payload).digest("hex").slice(0, 24);
325453
+ }
325454
+ function ensurePrivateDirectory2(dir) {
325455
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 448 });
325456
+ try {
325457
+ (0, node_fs_1.chmodSync)(dir, 448);
325458
+ } catch {
325459
+ }
325460
+ }
325461
+ function removeFile(path) {
325462
+ try {
325463
+ (0, node_fs_1.unlinkSync)(path);
325464
+ } catch {
325465
+ }
325466
+ }
325467
+ function readLockOwner(path) {
325468
+ try {
325469
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
325470
+ return typeof parsed.token === "string" && parsed.token.length > 0 && Number.isSafeInteger(parsed.pid) && (parsed.pid ?? 0) > 0 ? { token: parsed.token, pid: parsed.pid } : void 0;
325471
+ } catch {
325472
+ return void 0;
325473
+ }
325474
+ }
325475
+ function processIsAlive(pid) {
325476
+ try {
325477
+ process.kill(pid, 0);
325478
+ return true;
325479
+ } catch (error2) {
325480
+ return error2.code === "EPERM";
325481
+ }
325482
+ }
325483
+ function staleLockCanBeReaped(path, now, staleMs) {
325484
+ try {
325485
+ const ageMs = now - (0, node_fs_1.statSync)(path).mtimeMs;
325486
+ if (ageMs < staleMs)
325487
+ return false;
325488
+ const owner = readLockOwner(path);
325489
+ if (owner) {
325490
+ if (!processIsAlive(owner.pid))
325491
+ return true;
325492
+ return ageMs >= Math.max(staleMs * 5, exports.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS);
325493
+ }
325494
+ return ageMs >= staleMs * 5;
325495
+ } catch {
325496
+ return false;
325497
+ }
325498
+ }
325499
+ function isRecord(value) {
325500
+ return !!value && typeof value === "object" && !Array.isArray(value);
325501
+ }
325502
+ function isValidStorageState(value) {
325503
+ if (!isRecord(value))
325504
+ return false;
325505
+ const cookies = value.cookies;
325506
+ const origins = value.origins;
325507
+ if (!Array.isArray(cookies) || !Array.isArray(origins))
325508
+ return false;
325509
+ const cookiesValid = cookies.every((cookie) => {
325510
+ if (!isRecord(cookie))
325511
+ return false;
325512
+ return typeof cookie.name === "string" && typeof cookie.value === "string" && typeof cookie.domain === "string" && typeof cookie.path === "string" && typeof cookie.expires === "number" && Number.isFinite(cookie.expires) && typeof cookie.httpOnly === "boolean" && typeof cookie.secure === "boolean" && (cookie.sameSite === "Strict" || cookie.sameSite === "Lax" || cookie.sameSite === "None");
325513
+ });
325514
+ if (!cookiesValid)
325515
+ return false;
325516
+ const originsValid = origins.every((origin) => {
325517
+ if (!isRecord(origin) || typeof origin.origin !== "string" || !Array.isArray(origin.localStorage))
325518
+ return false;
325519
+ return origin.localStorage.every((entry) => isRecord(entry) && typeof entry.name === "string" && typeof entry.value === "string");
325520
+ });
325521
+ if (!originsValid)
325522
+ return false;
325523
+ return cookies.length > 0 || origins.some((origin) => origin.localStorage.length > 0);
325524
+ }
325525
+ function isValidStorageStateJson(value) {
325526
+ try {
325527
+ return isValidStorageState(JSON.parse(value));
325528
+ } catch {
325529
+ return false;
325530
+ }
325531
+ }
325532
+ function isStorageStateShape(value) {
325533
+ if (!value || typeof value !== "object")
325534
+ return false;
325535
+ return isValidStorageState(value);
325536
+ }
325537
+ function isValidLocalAuthStateFile(path) {
325538
+ try {
325539
+ return isStorageStateShape(JSON.parse((0, node_fs_1.readFileSync)(path, "utf8")));
325540
+ } catch {
325541
+ return false;
325542
+ }
325543
+ }
325544
+ function cleanupLocalAuthStateFiles(runsDir, projectId, now = Date.now(), maxAgeMs = exports.LOCAL_AUTH_STATE_MAX_AGE_MS) {
325545
+ ensurePrivateDirectory2(runsDir);
325546
+ const projectSegment = safeSegment(projectId, "default");
325547
+ const legacyName = `auth-state-${projectSegment}.json`;
325548
+ const prefix = `auth-state-${projectSegment}-`;
325549
+ for (const name of (0, node_fs_1.readdirSync)(runsDir)) {
325550
+ if (name === legacyName) {
325551
+ removeFile((0, node_path_1.join)(runsDir, name));
325552
+ continue;
325553
+ }
325554
+ if (!name.startsWith(prefix))
325555
+ continue;
325556
+ const path = (0, node_path_1.join)(runsDir, name);
325557
+ try {
325558
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs) {
325559
+ if (!name.endsWith(".lock") || staleLockCanBeReaped(path, now, maxAgeMs))
325560
+ removeFile(path);
325561
+ }
325562
+ } catch {
325563
+ }
325564
+ }
325565
+ }
325566
+ function cleanupExpiredLocalAuthStateFiles2(runsDir, now = Date.now(), maxAgeMs = exports.LOCAL_AUTH_STATE_MAX_AGE_MS) {
325567
+ ensurePrivateDirectory2(runsDir);
325568
+ for (const name of (0, node_fs_1.readdirSync)(runsDir)) {
325569
+ if (!name.startsWith("auth-state-"))
325570
+ continue;
325571
+ const path = (0, node_path_1.join)(runsDir, name);
325572
+ try {
325573
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs) {
325574
+ if (!name.endsWith(".lock") || staleLockCanBeReaped(path, now, maxAgeMs))
325575
+ removeFile(path);
325576
+ }
325577
+ } catch {
325578
+ }
325579
+ }
325580
+ }
325581
+ function localAuthStatePaths2(runsDir, input, runId) {
325582
+ cleanupLocalAuthStateFiles(runsDir, input.projectId);
325583
+ const projectSegment = safeSegment(input.projectId, "default");
325584
+ const runSegment = safeSegment(runId, `${process.pid}`);
325585
+ const fingerprint = localAuthStateFingerprint(input);
325586
+ const cachePath = (0, node_path_1.join)(runsDir, `auth-state-${projectSegment}-${fingerprint}.json`);
325587
+ return {
325588
+ cachePath,
325589
+ capturePath: `${cachePath}.capture-${runSegment}-${process.pid}.tmp`,
325590
+ fingerprint,
325591
+ lockKey: `${normalizeOrigin(input.serverUrl)}:${projectSegment}:${fingerprint}`,
325592
+ lockPath: `${cachePath}.lock`
325593
+ };
325594
+ }
325595
+ function getValidLocalAuthState2(path, now = Date.now(), maxAgeMs = exports.LOCAL_AUTH_STATE_MAX_AGE_MS) {
325596
+ if (!(0, node_fs_1.existsSync)(path))
325597
+ return void 0;
325598
+ try {
325599
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs || !isValidLocalAuthStateFile(path)) {
325600
+ removeFile(path);
325601
+ return void 0;
325602
+ }
325603
+ return path;
325604
+ } catch {
325605
+ return void 0;
325606
+ }
325607
+ }
325608
+ async function waitForValidLocalAuthState(path, timeoutMs = exports.LOCAL_AUTH_STATE_WAIT_MS, pollMs = 100) {
325609
+ const deadline = Date.now() + Math.max(0, timeoutMs);
325610
+ do {
325611
+ const valid = getValidLocalAuthState2(path);
325612
+ if (valid)
325613
+ return valid;
325614
+ const remaining = deadline - Date.now();
325615
+ if (remaining <= 0)
325616
+ return void 0;
325617
+ await new Promise((resolve2) => setTimeout(resolve2, Math.min(Math.max(10, pollMs), remaining)));
325618
+ } while (true);
325619
+ }
325620
+ async function waitForLocalAuthStateProducer2(path, lockKey, startupGraceMs = 2e3, maxWaitMs = exports.LOCAL_AUTH_STATE_WAIT_MS, pollMs = 100, lockPath) {
325621
+ const startedAt = Date.now();
325622
+ const producerIsActive = () => lockTails.has(lockKey) || !!lockPath && (0, node_fs_1.existsSync)(lockPath);
325623
+ let sawProducer = producerIsActive();
325624
+ do {
325625
+ const valid = getValidLocalAuthState2(path);
325626
+ if (valid)
325627
+ return valid;
325628
+ const producerActive = producerIsActive();
325629
+ if (producerActive)
325630
+ sawProducer = true;
325631
+ const elapsed = Date.now() - startedAt;
325632
+ if (sawProducer && !producerActive)
325633
+ return void 0;
325634
+ if (!sawProducer && elapsed >= Math.max(0, startupGraceMs))
325635
+ return void 0;
325636
+ if (elapsed >= Math.max(0, maxWaitMs))
325637
+ return void 0;
325638
+ await new Promise((resolve2) => setTimeout(resolve2, Math.max(10, pollMs)));
325639
+ } while (true);
325640
+ }
325641
+ function promoteLocalAuthStateCapture2(capturePath, cachePath, replaceExisting = false, lease) {
325642
+ if (lease && !lease.isOwner()) {
325643
+ removeFile(capturePath);
325644
+ return false;
325645
+ }
325646
+ if (!(0, node_fs_1.existsSync)(capturePath) || !isValidLocalAuthStateFile(capturePath)) {
325647
+ removeFile(capturePath);
325648
+ return false;
325649
+ }
325650
+ try {
325651
+ (0, node_fs_1.chmodSync)(capturePath, 384);
325652
+ } catch {
325653
+ }
325654
+ try {
325655
+ if (lease && !lease.isOwner()) {
325656
+ removeFile(capturePath);
325657
+ return false;
325658
+ }
325659
+ if ((0, node_fs_1.existsSync)(cachePath)) {
325660
+ if (!replaceExisting && isValidLocalAuthStateFile(cachePath)) {
325661
+ removeFile(capturePath);
325662
+ return true;
325663
+ }
325664
+ removeFile(cachePath);
325665
+ }
325666
+ if (lease && !lease.isOwner()) {
325667
+ removeFile(capturePath);
325668
+ return false;
325669
+ }
325670
+ (0, node_fs_1.renameSync)(capturePath, cachePath);
325671
+ try {
325672
+ (0, node_fs_1.chmodSync)(cachePath, 384);
325673
+ } catch {
325674
+ }
325675
+ return true;
325676
+ } catch {
325677
+ const winnerIsValid = (0, node_fs_1.existsSync)(cachePath) && isValidLocalAuthStateFile(cachePath);
325678
+ removeFile(capturePath);
325679
+ return winnerIsValid;
325680
+ }
325681
+ }
325682
+ function discardLocalAuthStateCapture2(capturePath) {
325683
+ removeFile(capturePath);
325684
+ }
325685
+ var lockTails = /* @__PURE__ */ new Map();
325686
+ function publishCompleteLockOwner(lockPath, owner, linkFile) {
325687
+ const metadata = JSON.stringify(owner);
325688
+ const tempPath = `${lockPath}.${owner.pid}.${owner.token}.tmp`;
325689
+ let createdLock = false;
325690
+ try {
325691
+ (0, node_fs_1.writeFileSync)(tempPath, metadata, { flag: "wx", mode: 384 });
325692
+ try {
325693
+ linkFile(tempPath, lockPath);
325694
+ createdLock = true;
325695
+ return (0, node_fs_1.openSync)(lockPath, "r+");
325696
+ } catch (error2) {
325697
+ const code = error2.code;
325698
+ if (!["EPERM", "EACCES", "EXDEV", "ENOSYS", "ENOTSUP", "EOPNOTSUPP"].includes(code ?? "")) {
325699
+ throw error2;
325700
+ }
325701
+ let fd;
325702
+ try {
325703
+ fd = (0, node_fs_1.openSync)(lockPath, "wx+", 384);
325704
+ createdLock = true;
325705
+ (0, node_fs_1.writeFileSync)(fd, metadata, { encoding: "utf8" });
325706
+ (0, node_fs_1.fsyncSync)(fd);
325707
+ return fd;
325708
+ } catch (fallbackError) {
325709
+ if (fd !== void 0) {
325710
+ try {
325711
+ (0, node_fs_1.closeSync)(fd);
325712
+ } catch {
325713
+ }
325714
+ }
325715
+ if (createdLock)
325716
+ removeFile(lockPath);
325717
+ throw fallbackError;
325718
+ }
325719
+ }
325720
+ } catch (error2) {
325721
+ if (createdLock && readLockOwner(lockPath)?.token === owner.token)
325722
+ removeFile(lockPath);
325723
+ throw error2;
325724
+ } finally {
325725
+ removeFile(tempPath);
325726
+ }
325727
+ }
325728
+ async function acquireExclusiveFileLock(lockPath, linkFile = node_fs_1.linkSync) {
325729
+ ensurePrivateDirectory2((0, node_path_1.dirname)(lockPath));
325730
+ const startedAt = Date.now();
325731
+ while (true) {
325732
+ try {
325733
+ const ownerToken = (0, node_crypto_1.randomBytes)(24).toString("base64url");
325734
+ const fd = publishCompleteLockOwner(lockPath, { token: ownerToken, pid: process.pid }, linkFile);
325735
+ const ownsPath = () => {
325736
+ try {
325737
+ const owner = readLockOwner(lockPath);
325738
+ return owner?.token === ownerToken && owner.pid === process.pid;
325739
+ } catch {
325740
+ return false;
325741
+ }
325742
+ };
325743
+ const heartbeat = setInterval(() => {
325744
+ try {
325745
+ if (!ownsPath()) {
325746
+ clearInterval(heartbeat);
325747
+ return;
325748
+ }
325749
+ const now = /* @__PURE__ */ new Date();
325750
+ (0, node_fs_1.futimesSync)(fd, now, now);
325751
+ } catch {
325752
+ clearInterval(heartbeat);
325753
+ }
325754
+ }, Math.max(1e3, Math.floor(exports.LOCAL_AUTH_STATE_LOCK_STALE_MS / 3)));
325755
+ heartbeat.unref();
325756
+ let released = false;
325757
+ const release = (() => {
325758
+ if (released)
325759
+ return;
325760
+ released = true;
325761
+ clearInterval(heartbeat);
325762
+ try {
325763
+ if (ownsPath())
325764
+ removeFile(lockPath);
325765
+ } catch {
325766
+ }
325767
+ try {
325768
+ (0, node_fs_1.closeSync)(fd);
325769
+ } catch {
325770
+ }
325771
+ });
325772
+ release.isOwner = () => !released && ownsPath();
325773
+ return release;
325774
+ } catch (error2) {
325775
+ const code = error2.code;
325776
+ if (code !== "EEXIST")
325777
+ throw error2;
325778
+ try {
325779
+ if (staleLockCanBeReaped(lockPath, Date.now(), exports.LOCAL_AUTH_STATE_LOCK_STALE_MS)) {
325780
+ removeFile(lockPath);
325781
+ continue;
325782
+ }
325783
+ } catch {
325784
+ continue;
325785
+ }
325786
+ if (Date.now() - startedAt >= exports.LOCAL_AUTH_STATE_WAIT_MS) {
325787
+ throw new Error(`Timed out waiting for auth-state lock: ${(0, node_path_1.basename)(lockPath)}`);
325788
+ }
325789
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
325790
+ }
325791
+ }
325792
+ }
325793
+ async function acquireLocalAuthStateLock2(key, lockPath, linkFile = node_fs_1.linkSync) {
325794
+ const previous = lockTails.get(key) ?? Promise.resolve();
325795
+ let unlockCurrent;
325796
+ const current = new Promise((resolve2) => {
325797
+ unlockCurrent = resolve2;
325798
+ });
325799
+ const tail = previous.then(() => current);
325800
+ lockTails.set(key, tail);
325801
+ await previous;
325802
+ let releaseFileLock;
325803
+ try {
325804
+ if (lockPath)
325805
+ releaseFileLock = await acquireExclusiveFileLock(lockPath, linkFile);
325806
+ } catch (error2) {
325807
+ unlockCurrent();
325808
+ if (lockTails.get(key) === tail)
325809
+ lockTails.delete(key);
325810
+ throw error2;
325811
+ }
325812
+ let released = false;
325813
+ const release = (() => {
325814
+ if (released)
325815
+ return;
325816
+ released = true;
325817
+ releaseFileLock?.();
325818
+ unlockCurrent();
325819
+ if (lockTails.get(key) === tail)
325820
+ lockTails.delete(key);
325821
+ });
325822
+ release.isOwner = () => !released && (releaseFileLock?.isOwner() ?? true);
325823
+ return release;
325824
+ }
325825
+ function localAuthStateFilename(path) {
325826
+ return (0, node_path_1.basename)(path);
325827
+ }
325828
+ }
325829
+ });
325830
+
324813
325831
  // ../../node_modules/adm-zip/util/constants.js
324814
325832
  var require_constants5 = __commonJS({
324815
325833
  "../../node_modules/adm-zip/util/constants.js"(exports, module) {
@@ -325123,7 +326141,7 @@ var require_utils2 = __commonJS({
325123
326141
  module.exports = Utils;
325124
326142
  Utils.prototype.makeDir = function(folder) {
325125
326143
  const self = this;
325126
- function mkdirSync5(fpath) {
326144
+ function mkdirSync4(fpath) {
325127
326145
  let resolvedPath = fpath.split(self.sep)[0];
325128
326146
  fpath.split(self.sep).forEach(function(name) {
325129
326147
  if (!name || name.substr(-1, 1) === ":") return;
@@ -325141,7 +326159,7 @@ var require_utils2 = __commonJS({
325141
326159
  if (stat && stat.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
325142
326160
  });
325143
326161
  }
325144
- mkdirSync5(folder);
326162
+ mkdirSync4(folder);
325145
326163
  };
325146
326164
  Utils.prototype.writeFileTo = function(path, content, overwrite, attr) {
325147
326165
  const self = this;
@@ -337065,6 +338083,8 @@ var require_dist2 = __commonJS({
337065
338083
  __exportStar(require_discover_agent(), exports);
337066
338084
  __exportStar(require_browser_pool(), exports);
337067
338085
  __exportStar(require_resource_governor(), exports);
338086
+ __exportStar(require_local_auth_state(), exports);
338087
+ __exportStar(require_run_callback_proxy(), exports);
337068
338088
  __exportStar(require_discover_explorer(), exports);
337069
338089
  __exportStar(require_discover_feature_explorer(), exports);
337070
338090
  __exportStar(require_discover_planner(), exports);
@@ -337341,7 +338361,7 @@ var require_package4 = __commonJS({
337341
338361
  "package.json"(exports, module) {
337342
338362
  module.exports = {
337343
338363
  name: "@validate.qa/runner",
337344
- version: "1.0.42",
338364
+ version: "1.0.44",
337345
338365
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
337346
338366
  bin: {
337347
338367
  "validate-runner": "dist/cli.js"
@@ -337349,7 +338369,7 @@ var require_package4 = __commonJS({
337349
338369
  scripts: {
337350
338370
  build: "tsup",
337351
338371
  dev: "tsx src/cli.ts",
337352
- test: "node --import tsx --test src/version-check.test.ts",
338372
+ test: "node --import tsx --test src/version-check.test.ts src/services/auth-state.test.ts",
337353
338373
  "test:mobile": "node --import tsx --test --test-concurrency=2 src/runner.mobile-ai-env.test.ts src/mobile-device-queue.test.ts src/device-lease.test.ts src/result-payload-budget.test.ts src/services/appium-*.test.ts src/services/*mobile*.test.ts src/services/mobile/*.test.ts",
337354
338374
  prepublishOnly: "npm run build"
337355
338375
  },
@@ -337428,7 +338448,6 @@ function getConfigDir() {
337428
338448
  }
337429
338449
 
337430
338450
  // src/runner.ts
337431
- import { existsSync as existsSync3, statSync, mkdirSync as mkdirSync4 } from "fs";
337432
338451
  import { join as join6 } from "path";
337433
338452
  import { homedir as homedir3 } from "os";
337434
338453
  import { createHash } from "crypto";
@@ -337456,26 +338475,28 @@ var import_runner_core6 = __toESM(require_dist2());
337456
338475
 
337457
338476
  // src/runner.ts
337458
338477
  var import_runner_core11 = __toESM(require_dist2());
338478
+ var import_runner_core12 = __toESM(require_dist2());
337459
338479
 
337460
338480
  // src/services/auth-state.ts
337461
- var INLINE_LOGIN_TAG = "@inline-login";
338481
+ init_dist();
337462
338482
  var GUARDED_LOGIN_TAG = "@auth-guarded";
337463
338483
  function sharedAuthInjectEnabled() {
337464
- return process.env.SHARED_AUTH_INJECT === "true";
338484
+ return isSharedAuthInjectEnabled();
337465
338485
  }
337466
- function hasInlineLoginSteps(playwrightCode) {
338486
+ function hasGeneratedAuthGuard2(playwrightCode) {
337467
338487
  if (!playwrightCode) return false;
337468
- return /process\.env\.TEST_USER_EMAIL!?/.test(playwrightCode) && /process\.env\.TEST_USER_PASSWORD!?/.test(playwrightCode);
338488
+ return /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
338489
+ }
338490
+ function shouldCaptureAuthState(tags, playwrightCode) {
338491
+ return sharedAuthInjectEnabled() && Array.isArray(tags) && tags.includes("@needs-auth") && tags.includes(GUARDED_LOGIN_TAG) && !tags.includes("@auth") && hasGeneratedAuthGuard2(playwrightCode);
337469
338492
  }
337470
338493
  function shouldUseAuthState(tags, playwrightCode) {
338494
+ if (!sharedAuthInjectEnabled()) return false;
337471
338495
  if (!Array.isArray(tags)) return false;
337472
- if (tags.includes("@auth")) return false;
337473
- if (tags.includes("@api")) return true;
337474
- if (!tags.includes("@needs-auth")) return false;
337475
- if (sharedAuthInjectEnabled() && tags.includes(GUARDED_LOGIN_TAG)) return true;
337476
- const isGeneratedVariant = tags.includes("@edge") || tags.includes("@bug");
337477
- const hasLegacyInlineLogin = tags.includes("@discovered") && !isGeneratedVariant && hasInlineLoginSteps(playwrightCode);
337478
- return !tags.includes(INLINE_LOGIN_TAG) && !hasLegacyInlineLogin;
338496
+ return authPlanFor(tags, playwrightCode).needsCookieState;
338497
+ }
338498
+ function authPlanFor(tags, playwrightCode) {
338499
+ return deriveAuthPlan(tags, playwrightCode, { sharedAuthInjectEnabled: sharedAuthInjectEnabled() });
337479
338500
  }
337480
338501
 
337481
338502
  // src/version-check.ts
@@ -344185,8 +345206,8 @@ function withMediaReferences(result, refs) {
344185
345206
 
344186
345207
  // src/runner.ts
344187
345208
  init_dist();
344188
- var import_runner_core12 = __toESM(require_dist2());
344189
345209
  var import_runner_core13 = __toESM(require_dist2());
345210
+ var import_runner_core14 = __toESM(require_dist2());
344190
345211
  function fingerprintTest(test) {
344191
345212
  const code = test.framework === "APPIUM" ? [test.appiumCode ?? "", JSON.stringify(test.steps ?? [])].join("\n--steps--\n") : test.playwrightCode ?? "";
344192
345213
  return createHash("sha256").update(test.name).update("\n--suite--\n").update(test.suiteName ?? "").update("\n--type--\n").update(test.testType ?? "").update("\n--framework--\n").update(test.framework ?? "").update("\n--code--\n").update(code).digest("hex").slice(0, 24);
@@ -344310,21 +345331,11 @@ var log = {
344310
345331
  warn: (msg) => console.log(`${C2.yellow}\u26A0${C2.reset} ${msg}`),
344311
345332
  dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
344312
345333
  };
344313
- var AUTH_MAX_AGE_MS = 60 * 60 * 1e3;
344314
345334
  function getRunsDir() {
344315
345335
  const dir = process.env.RUNS_DIR || join6(homedir3(), ".validate.qa", "runs");
344316
- if (!existsSync3(dir)) mkdirSync4(dir, { recursive: true });
345336
+ (0, import_runner_core12.ensurePrivateDirectory)(dir);
344317
345337
  return dir;
344318
345338
  }
344319
- function authStatePath(projectId) {
344320
- return join6(getRunsDir(), `auth-state-${projectId || "default"}.json`);
344321
- }
344322
- function getValidAuthState(projectId) {
344323
- const path = authStatePath(projectId);
344324
- if (!existsSync3(path)) return void 0;
344325
- const age = Date.now() - statSync(path).mtimeMs;
344326
- return age < AUTH_MAX_AGE_MS ? path : void 0;
344327
- }
344328
345339
  function buildHeaders(authToken, projectId) {
344329
345340
  return {
344330
345341
  "Content-Type": "application/json",
@@ -344373,7 +345384,13 @@ async function claimRun(serverUrl, headers, runId) {
344373
345384
  if (res.status === 429) return false;
344374
345385
  if (!res.ok) throw new Error(`Claim failed: ${res.status}`);
344375
345386
  const body = await res.json();
344376
- return body.claimed;
345387
+ if (!body.claimed) {
345388
+ throw new Error("Claim response did not confirm ownership");
345389
+ }
345390
+ if (typeof body.claimEpoch !== "number" || !Number.isSafeInteger(body.claimEpoch) || body.claimEpoch < 0) {
345391
+ throw new Error("Claim response did not include a valid ownership epoch");
345392
+ }
345393
+ return body.claimEpoch;
344377
345394
  }
344378
345395
  async function postResult(serverUrl, headers, runId, result) {
344379
345396
  const MAX_ATTEMPTS = 3;
@@ -344541,12 +345558,12 @@ async function postCaptureResult(serverUrl, headers, runId, result) {
344541
345558
  });
344542
345559
  if (!res.ok) throw new Error(`Failed to post capture result: ${res.status}`);
344543
345560
  }
344544
- async function postCredentialRotation(serverUrl, headers, runId, body) {
345561
+ async function postCredentialRotation(serverUrl, headers, runId, body, claimEpoch) {
344545
345562
  try {
344546
345563
  const res = await fetch(`${serverUrl}/api/runner/runs/${runId}/credential`, {
344547
345564
  method: "POST",
344548
345565
  headers,
344549
- body: JSON.stringify(body),
345566
+ body: JSON.stringify({ ...body, ...claimEpoch !== void 0 ? { claimEpoch } : {} }),
344550
345567
  signal: AbortSignal.timeout(3e4)
344551
345568
  });
344552
345569
  if (!res.ok) {
@@ -344878,11 +345895,28 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
344878
345895
  requestLiveBrowserHeartbeat();
344879
345896
  }, 5e3);
344880
345897
  liveBrowserHeartbeatInterval.unref?.();
345898
+ const authPlan = authPlanFor(run2.tags, run2.playwrightCode);
345899
+ const runAuthCachePath = (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
345900
+ projectId: run2.projectId || opts.projectId || "default",
345901
+ serverUrl: opts.serverUrl,
345902
+ baseUrl: run2.baseUrl,
345903
+ testVariables: run2.testVariables,
345904
+ authToken: opts.authToken
345905
+ }, run2.runId).cachePath;
345906
+ const wrapperStorageStatePath = authPlan.needsCookieState && runMode !== "auth-setup" ? (0, import_runner_core12.getValidLocalAuthState)(runAuthCachePath) : void 0;
344881
345907
  const runNativeWithLiveBrowser = (nativeOptions) => (0, import_runner_core.executeTestViaNativePlaywright)({
345908
+ authStandingMode: authPlan.standingHeadersMode,
345909
+ storageStatePath: wrapperStorageStatePath,
344882
345910
  ...nativeOptions,
344883
345911
  liveBrowserHandle,
344884
345912
  onLiveBrowserSnapshot: requestLiveBrowserHeartbeat
344885
345913
  });
345914
+ const callbackContext = {
345915
+ runId: run2.runId,
345916
+ serverUrl: opts.serverUrl,
345917
+ authHeader: headers.Authorization,
345918
+ claimEpoch: run2.claimEpoch
345919
+ };
344886
345920
  const startTime = Date.now();
344887
345921
  try {
344888
345922
  if (mobileLeaseError) {
@@ -344900,11 +345934,24 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
344900
345934
  if (!run2.baseUrl) {
344901
345935
  preflightError = "Pre-flight failed: Missing BASE_URL. Configure it in Test Data \u2192 Test Variables.";
344902
345936
  } else {
344903
- const needsAuth = Array.isArray(run2.tags) && run2.tags.some((t) => t === "@auth" || t === "@needs-auth" || t === "@api");
344904
- if (needsAuth && !run2.testVariables?.["TEST_USER_EMAIL"]) {
344905
- preflightError = "Pre-flight failed: Missing TEST_USER_EMAIL. Tests tagged @auth, @needs-auth, or @api require login credentials.";
344906
- } else if (needsAuth && !run2.testVariables?.["TEST_USER_PASSWORD"]) {
344907
- preflightError = "Pre-flight failed: Missing TEST_USER_PASSWORD. Tests tagged @auth, @needs-auth, or @api require login credentials.";
345937
+ const validStanding = (0, import_runner_core13.parseApiAuthHeaders)(run2.testVariables?.["API_AUTH_HEADERS"]) !== void 0;
345938
+ if (authPlan.configurationError) {
345939
+ preflightError = `Pre-flight failed: ${authPlan.configurationError}. Fix the test's auth-type tag before running.`;
345940
+ } else if (authPlan.needsStandingHeaders && !validStanding) {
345941
+ preflightError = "Pre-flight failed: This @auth-standing @api test requires a valid API_AUTH_HEADERS JSON object (header name \u2192 value).";
345942
+ } else {
345943
+ const legacyHeaderBypass = authPlan.source === "legacy" && authPlan.standingHeadersMode === "legacy-if-configured" && validStanding;
345944
+ const code = run2.playwrightCode ?? "";
345945
+ const codeProvided = code.trim().length > 0;
345946
+ const codeUsesCreds = codeProvided && /TEST_USER_EMAIL|TEST_USER_PASSWORD|__vqaLogin/.test(code);
345947
+ const tags = Array.isArray(run2.tags) ? run2.tags : [];
345948
+ const isHallucinationShape = codeProvided && !codeUsesCreds && tags.includes("@inline-login") && !tags.includes("@api");
345949
+ const requireCreds = authPlan.needsLoginCreds && !legacyHeaderBypass && !isHallucinationShape;
345950
+ if (requireCreds && !run2.testVariables?.["TEST_USER_EMAIL"]?.trim()) {
345951
+ preflightError = "Pre-flight failed: Missing TEST_USER_EMAIL. Tests that log in (tagged @auth, @needs-auth, @api, @auth-cookie-state, @auth-login-cookie, or @auth-login-bearer) require login credentials.";
345952
+ } else if (requireCreds && !run2.testVariables?.["TEST_USER_PASSWORD"]?.trim()) {
345953
+ preflightError = "Pre-flight failed: Missing TEST_USER_PASSWORD. Tests that log in (tagged @auth, @needs-auth, @api, @auth-cookie-state, @auth-login-cookie, or @auth-login-bearer) require login credentials.";
345954
+ }
344908
345955
  }
344909
345956
  }
344910
345957
  if (preflightError) {
@@ -344912,7 +345959,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
344912
345959
  log.fail(`PRE-FLIGHT FAILED - ${preflightError}`);
344913
345960
  return;
344914
345961
  }
344915
- const preflight = await (0, import_runner_core12.resolveBaseUrlPreflight)(run2.baseUrl);
345962
+ const preflight = await (0, import_runner_core13.resolveBaseUrlPreflight)(run2.baseUrl);
344916
345963
  if (preflight.error) {
344917
345964
  await postResult(opts.serverUrl, headers, run2.runId, { status: "ERROR", error: preflight.error });
344918
345965
  log.fail(`PRE-FLIGHT FAILED - ${preflight.error}`);
@@ -344929,31 +345976,72 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
344929
345976
  log.fail("ERROR - No auth setup code");
344930
345977
  return;
344931
345978
  }
344932
- const runAuthPath = authStatePath(run2.projectId || opts.projectId || "default");
344933
- log.info(`Auth setup: saving session state to ${runAuthPath}`);
344934
- const result2 = await runNativeWithLiveBrowser({
344935
- playwrightCode: run2.playwrightCode,
345979
+ const sharedAuthEnabled = sharedAuthInjectEnabled();
345980
+ if ((0, import_runner_core12.shouldSkipDisabledAutomaticAuthSetup)(run2.authForceRun, run2.authVerify, sharedAuthEnabled)) {
345981
+ const duration2 = Date.now() - startTime;
345982
+ await postResult(opts.serverUrl, headers, run2.runId, {
345983
+ status: "PASSED",
345984
+ logs: "Automatic auth prewarm skipped: shared session reuse is disabled.",
345985
+ duration: duration2
345986
+ });
345987
+ log.info(`Auth prewarm skipped (${duration2}ms) - SHARED_AUTH_INJECT=false`);
345988
+ return;
345989
+ }
345990
+ const authPaths2 = sharedAuthEnabled ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
345991
+ projectId: run2.projectId || opts.projectId || "default",
345992
+ serverUrl: opts.serverUrl,
344936
345993
  baseUrl: run2.baseUrl,
344937
- testName: "auth-setup",
344938
- testVariables: { ...run2.testVariables, AUTH_FILE: runAuthPath },
344939
- validateContext: {
344940
- runId: run2.runId,
344941
- serverUrl: opts.serverUrl,
344942
- authHeader: headers.Authorization
345994
+ testVariables: run2.testVariables,
345995
+ authToken: opts.authToken
345996
+ }, run2.runId) : void 0;
345997
+ const releaseAuthLock2 = authPaths2 ? await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths2.lockKey, authPaths2.lockPath) : void 0;
345998
+ try {
345999
+ if (authPaths2 && (0, import_runner_core12.shouldSkipWarmAuthSetup)(
346000
+ run2.authForceRun,
346001
+ sharedAuthEnabled,
346002
+ !!(0, import_runner_core12.getValidLocalAuthState)(authPaths2.cachePath)
346003
+ )) {
346004
+ const duration3 = Date.now() - startTime;
346005
+ await postResult(opts.serverUrl, headers, run2.runId, {
346006
+ status: "PASSED",
346007
+ logs: "Auth setup skipped: the credential-bound local session is already warm.",
346008
+ duration: duration3
346009
+ });
346010
+ log.ok(`Auth setup skipped (${duration3}ms) - local session already warm`);
346011
+ return;
344943
346012
  }
344944
- });
344945
- const duration2 = Date.now() - startTime;
344946
- await postResult(opts.serverUrl, headers, run2.runId, {
344947
- status: result2.passed ? "PASSED" : "FAILED",
344948
- apiCalls: result2.apiCalls,
344949
- logs: result2.logs,
344950
- error: result2.error,
344951
- duration: duration2
344952
- });
344953
- log[result2.passed ? "ok" : "fail"](
344954
- `Auth setup ${result2.passed ? "PASSED" : "FAILED"} (${duration2}ms)${result2.error ? ` - ${result2.error}` : ""}`
344955
- );
344956
- return;
346013
+ log.info(run2.authVerify ? "Auth verification: replaying the current login recipe" : sharedAuthEnabled ? "Auth setup: capturing a credential-bound local session" : "Auth setup: running login with shared session reuse disabled");
346014
+ const capturePath = sharedAuthEnabled && !run2.authVerify ? authPaths2?.capturePath : void 0;
346015
+ const result2 = await runNativeWithLiveBrowser({
346016
+ playwrightCode: run2.playwrightCode,
346017
+ baseUrl: run2.baseUrl,
346018
+ testName: "auth-setup",
346019
+ testVariables: run2.testVariables,
346020
+ authStateCapturePath: capturePath,
346021
+ validateContext: callbackContext
346022
+ });
346023
+ const captured = !!capturePath && !!authPaths2 && result2.passed && !result2.suspiciousPass && (0, import_runner_core12.promoteLocalAuthStateCapture)(
346024
+ capturePath,
346025
+ authPaths2.cachePath,
346026
+ run2.authForceRun === true,
346027
+ releaseAuthLock2
346028
+ );
346029
+ const duration2 = Date.now() - startTime;
346030
+ await postResult(opts.serverUrl, headers, run2.runId, {
346031
+ status: result2.passed ? "PASSED" : "FAILED",
346032
+ apiCalls: result2.apiCalls,
346033
+ logs: result2.logs,
346034
+ error: result2.error,
346035
+ duration: duration2
346036
+ });
346037
+ log[result2.passed ? "ok" : "fail"](
346038
+ `Auth setup ${result2.passed ? "PASSED" : "FAILED"} (${duration2}ms)${captured ? " - session cached" : ""}${result2.error ? ` - ${result2.error}` : ""}`
346039
+ );
346040
+ return;
346041
+ } finally {
346042
+ if (authPaths2?.capturePath) (0, import_runner_core12.discardLocalAuthStateCapture)(authPaths2.capturePath);
346043
+ releaseAuthLock2?.();
346044
+ }
344957
346045
  }
344958
346046
  if (runMode === "batch-heal") {
344959
346047
  const tests = run2.batchHealTests ?? [];
@@ -344972,7 +346060,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
344972
346060
  const batchResult = await (0, import_runner_core10.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
344973
346061
  testVariables: run2.testVariables,
344974
346062
  runNativeTest: runNativeWithLiveBrowser,
344975
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
346063
+ validateContext: callbackContext,
344976
346064
  onLog: healLogStream.onLog
344977
346065
  });
344978
346066
  await healLogStream.flushNow();
@@ -345008,6 +346096,15 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
345008
346096
  );
345009
346097
  const describeKind = isSerialDescribe ? "serial" : "non-serial";
345010
346098
  log.info(`API test heal - ${subTestCount} sub-test(s) [${describeKind}], ${failingSubTestCount} budgeted, ${MAX_API_HEAL_ATTEMPTS} attempts`);
346099
+ const apiHealAuthPaths = shouldUseAuthState(run2.tags, run2.playwrightCode) ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
346100
+ projectId: run2.projectId || opts.projectId,
346101
+ serverUrl: opts.serverUrl,
346102
+ baseUrl: run2.baseUrl,
346103
+ testVariables: run2.testVariables,
346104
+ authToken: opts.authToken
346105
+ }, run2.runId) : void 0;
346106
+ const apiHealStorageStatePath = apiHealAuthPaths ? (0, import_runner_core12.getValidLocalAuthState)(apiHealAuthPaths.cachePath) : void 0;
346107
+ if (apiHealStorageStatePath) log.info("API heal verification is using saved auth state");
345011
346108
  const stickyAppBugs = /* @__PURE__ */ new Map();
345012
346109
  const lastChangeByTitle = /* @__PURE__ */ new Map();
345013
346110
  let currentError = run2.lastFailureError ?? "Test failed";
@@ -345073,7 +346170,8 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
345073
346170
  playwrightCode: lastHealedCode,
345074
346171
  baseUrl: run2.baseUrl,
345075
346172
  testVariables: run2.testVariables,
345076
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization }
346173
+ storageStatePath: apiHealStorageStatePath,
346174
+ validateContext: callbackContext
345077
346175
  });
345078
346176
  if (finalVerifyResult.passed) {
345079
346177
  log.ok(` Attempt ${attempt} terminal verification PASSED (${bugCount} app_bug, ${passCount} pass)`);
@@ -345093,7 +346191,9 @@ ${finalVerifyResult.logs ?? ""}`;
345093
346191
  finalVerifyResult = await runNativeWithLiveBrowser({
345094
346192
  playwrightCode: lastHealedCode,
345095
346193
  baseUrl: run2.baseUrl,
345096
- testVariables: run2.testVariables
346194
+ testVariables: run2.testVariables,
346195
+ storageStatePath: apiHealStorageStatePath,
346196
+ validateContext: callbackContext
345097
346197
  });
345098
346198
  const stepResults = finalVerifyResult.stepResults ?? [];
345099
346199
  const nonAppBugFails = stepResults.filter(
@@ -345143,7 +346243,8 @@ ${finalVerifyResult.logs ?? ""}`;
345143
346243
  playwrightCode: lastHealedCode,
345144
346244
  baseUrl: run2.baseUrl,
345145
346245
  testVariables: run2.testVariables,
345146
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization }
346246
+ storageStatePath: apiHealStorageStatePath,
346247
+ validateContext: callbackContext
345147
346248
  });
345148
346249
  if (finalVerifyResult.passed) {
345149
346250
  log.ok(` Attempt ${attempt} verification PASSED`);
@@ -345196,7 +346297,7 @@ ${finalVerifyResult.logs ?? ""}`;
345196
346297
  if (classifyErrorMessage(composedError ?? null) === "config_issue") {
345197
346298
  apiHealStatus = "ERROR";
345198
346299
  } else {
345199
- const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
346300
+ const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
345200
346301
  testName: run2.testName ?? run2.testCaseId ?? void 0,
345201
346302
  tags: run2.tags ?? [],
345202
346303
  error: composedError ?? finalVerifyResult?.error ?? null,
@@ -345293,8 +346394,8 @@ ${finalVerifyResult.logs ?? ""}`;
345293
346394
  lastHealAttempt: run2.lastHealAttempt ?? null,
345294
346395
  lastHealedCode: run2.lastHealedCode ?? null,
345295
346396
  failureScreenshot: run2.failureScreenshot ?? null,
345296
- onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }),
345297
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
346397
+ onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }, run2.claimEpoch),
346398
+ validateContext: callbackContext,
345298
346399
  liveBrowserHandle,
345299
346400
  runNativeTest: runNativeWithLiveBrowser
345300
346401
  };
@@ -345336,7 +346437,7 @@ ${finalVerifyResult.logs ?? ""}`;
345336
346437
  let healStatus = result2.passed ? "PASSED" : isConfigIssue ? "ERROR" : "FAILED";
345337
346438
  let healError = result2.error;
345338
346439
  if (!result2.passed && !isConfigIssue) {
345339
- const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
346440
+ const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
345340
346441
  testName: run2.testName ?? run2.testCaseId ?? void 0,
345341
346442
  tags: run2.tags ?? [],
345342
346443
  error: result2.error,
@@ -345407,7 +346508,8 @@ ${finalVerifyResult.logs ?? ""}`;
345407
346508
  // Allow generated `persistCredential(...)` calls inside the suite to
345408
346509
  // POST back to the server with the runner's bearer token.
345409
346510
  validateServerUrl: opts.serverUrl,
345410
- validateRunnerAuthHeader: headers.Authorization
346511
+ validateRunnerAuthHeader: headers.Authorization,
346512
+ claimEpoch: run2.claimEpoch
345411
346513
  });
345412
346514
  const overall = result2.error && result2.testResults.length === 0 ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
345413
346515
  const rr = await fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/regression-result`, {
@@ -345458,11 +346560,14 @@ ${finalVerifyResult.logs ?? ""}`;
345458
346560
  });
345459
346561
  if (healRes.ok) {
345460
346562
  const { healedCode } = await healRes.json();
346563
+ const failedTestPlan = authPlanFor(failedTest.tags, healedCode);
345461
346564
  const verifyResult = await runNativeWithLiveBrowser({
345462
346565
  playwrightCode: healedCode,
345463
346566
  baseUrl: run2.baseUrl,
345464
346567
  testVariables: run2.testVariables,
345465
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization }
346568
+ storageStatePath: failedTestPlan.needsCookieState ? (0, import_runner_core12.getValidLocalAuthState)(runAuthCachePath) : void 0,
346569
+ authStandingMode: failedTestPlan.standingHeadersMode,
346570
+ validateContext: callbackContext
345466
346571
  });
345467
346572
  results.push({
345468
346573
  testCaseId: failedTest.testCaseId,
@@ -345525,8 +346630,8 @@ ${finalVerifyResult.logs ?? ""}`;
345525
346630
  onAICall: healAudit,
345526
346631
  modelOverride: run2.healerModel,
345527
346632
  consecutiveFails: failedTest.consecutiveFails,
345528
- onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }),
345529
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
346633
+ onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }, run2.claimEpoch),
346634
+ validateContext: callbackContext,
345530
346635
  liveBrowserHandle,
345531
346636
  runNativeTest: runNativeWithLiveBrowser
345532
346637
  };
@@ -345731,6 +346836,13 @@ ${finalVerifyResult.logs ?? ""}`;
345731
346836
  }
345732
346837
  return;
345733
346838
  }
346839
+ ctx.onCredentialUpdate = async (input) => postCredentialRotation(
346840
+ opts.serverUrl,
346841
+ headers,
346842
+ run2.runId,
346843
+ { ...input, actor: "ai-explorer" },
346844
+ run2.claimEpoch
346845
+ );
345734
346846
  log.info(`Discovery (${ctx.mode}) on ${ctx.baseUrl}`);
345735
346847
  const { push: pushAudit, drain: drainAudit } = createAuditFlusher(
345736
346848
  `${opts.serverUrl}/api/runner/runs/${run2.runId}/ai-audit-batch`,
@@ -346433,7 +347545,7 @@ ${finalVerifyResult.logs ?? ""}`;
346433
347545
  featureDescription: ctx.featureDescription,
346434
347546
  featurePages: ctx.featurePages,
346435
347547
  appBrief: ctx.appBrief,
346436
- onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-capture" })
347548
+ onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-capture" }, run2.claimEpoch)
346437
347549
  });
346438
347550
  if (logBuffer.length > 0) {
346439
347551
  postCaptureLog(opts.serverUrl, headers, run2.runId, ctx.jobId, truncateTextTail(logBuffer.join("\n"), DISCOVERY_LOG_POST_MAX_CHARS, "capture log"));
@@ -346678,7 +347790,7 @@ ${finalVerifyResult.logs ?? ""}`;
346678
347790
  }
346679
347791
  return verify;
346680
347792
  };
346681
- return await (0, import_runner_core13.executeMobileLiveHeal)({
347793
+ return await (0, import_runner_core14.executeMobileLiveHeal)({
346682
347794
  steps: a.steps,
346683
347795
  target: a.target,
346684
347796
  testVariables: a.testVariables,
@@ -346695,7 +347807,7 @@ ${finalVerifyResult.logs ?? ""}`;
346695
347807
  }
346696
347808
  };
346697
347809
  log.info("\u{1FA7A} Mobile AI heal \u2014 Phase 1 (script) \u2192 Phase 2 (live device, lazy)...");
346698
- const heal = await (0, import_runner_core13.executeMobileTestWithHealing)(
347810
+ const heal = await (0, import_runner_core14.executeMobileTestWithHealing)(
346699
347811
  mobileSteps,
346700
347812
  healTarget,
346701
347813
  {
@@ -346708,7 +347820,7 @@ ${finalVerifyResult.logs ?? ""}`;
346708
347820
  healerOptions
346709
347821
  );
346710
347822
  const finalRes = nativeVerify.last;
346711
- const outcome = (0, import_runner_core13.computeMobileHealOutcome)({
347823
+ const outcome = (0, import_runner_core14.computeMobileHealOutcome)({
346712
347824
  heal,
346713
347825
  finalError: finalRes?.error ?? null,
346714
347826
  reproduceError,
@@ -346854,26 +347966,77 @@ ${result2.logs}`);
346854
347966
  }
346855
347967
  return;
346856
347968
  }
346857
- const storageStatePath = shouldUseAuthState(run2.tags, run2.playwrightCode) ? getValidAuthState(run2.projectId || opts.projectId) : void 0;
346858
- if (storageStatePath) {
346859
- log.info(`Using auth state for project ${run2.projectId || opts.projectId}`);
346860
- }
346861
- const result = await runNativeWithLiveBrowser({
346862
- playwrightCode: run2.playwrightCode,
347969
+ const projectId = run2.projectId || opts.projectId;
347970
+ const authStateExpected = shouldUseAuthState(run2.tags, run2.playwrightCode);
347971
+ const captureAuth = shouldCaptureAuthState(run2.tags, run2.playwrightCode);
347972
+ const authPaths = authStateExpected || captureAuth ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
347973
+ projectId,
347974
+ serverUrl: opts.serverUrl,
346863
347975
  baseUrl: run2.baseUrl,
346864
347976
  testVariables: run2.testVariables,
346865
- storageStatePath,
346866
- validateContext: {
346867
- runId: run2.runId,
346868
- serverUrl: opts.serverUrl,
346869
- authHeader: headers.Authorization
347977
+ authToken: opts.authToken
347978
+ }, run2.runId) : void 0;
347979
+ let releaseAuthLock;
347980
+ let storageStatePath;
347981
+ if (authPaths && authStateExpected) {
347982
+ releaseAuthLock = await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths.lockKey, authPaths.lockPath);
347983
+ storageStatePath = (0, import_runner_core12.getValidLocalAuthState)(authPaths.cachePath);
347984
+ if (storageStatePath || !captureAuth) {
347985
+ releaseAuthLock();
347986
+ releaseAuthLock = void 0;
347987
+ }
347988
+ if (!storageStatePath && !captureAuth && sharedAuthInjectEnabled()) {
347989
+ log.info(`Waiting for the auth producer to warm project ${projectId}`);
347990
+ storageStatePath = await (0, import_runner_core12.waitForLocalAuthStateProducer)(
347991
+ authPaths.cachePath,
347992
+ authPaths.lockKey,
347993
+ 2e3,
347994
+ void 0,
347995
+ 100,
347996
+ authPaths.lockPath
347997
+ );
346870
347998
  }
346871
- });
347999
+ }
348000
+ if (storageStatePath) {
348001
+ log.info(`Using auth state for project ${projectId}`);
348002
+ }
348003
+ const runAuthPath = captureAuth ? authPaths?.capturePath : void 0;
348004
+ const result = await (async () => {
348005
+ try {
348006
+ return await runNativeWithLiveBrowser({
348007
+ playwrightCode: run2.playwrightCode,
348008
+ baseUrl: run2.baseUrl,
348009
+ testVariables: run2.testVariables,
348010
+ authStateCapturePath: runAuthPath,
348011
+ storageStatePath,
348012
+ validateContext: callbackContext
348013
+ });
348014
+ } finally {
348015
+ if (runAuthPath && authPaths) {
348016
+ if ((0, import_runner_core12.getValidLocalAuthState)(runAuthPath)) {
348017
+ const releasePromotionLock = releaseAuthLock ? void 0 : await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths.lockKey, authPaths.lockPath);
348018
+ try {
348019
+ const captured = (0, import_runner_core12.promoteLocalAuthStateCapture)(
348020
+ runAuthPath,
348021
+ authPaths.cachePath,
348022
+ !!storageStatePath,
348023
+ releaseAuthLock ?? releasePromotionLock
348024
+ );
348025
+ if (captured) log.info(`Captured auth state for project ${projectId}`);
348026
+ } finally {
348027
+ releasePromotionLock?.();
348028
+ }
348029
+ }
348030
+ (0, import_runner_core12.discardLocalAuthStateCapture)(runAuthPath);
348031
+ }
348032
+ releaseAuthLock?.();
348033
+ }
348034
+ })();
346872
348035
  const duration = Date.now() - startTime;
346873
348036
  let runStatus = result.passed ? "PASSED" : "FAILED";
346874
348037
  let runError = result.error;
346875
348038
  if (!result.passed) {
346876
- const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
348039
+ const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
346877
348040
  testName: run2.testName ?? run2.testCaseId ?? void 0,
346878
348041
  tags: run2.tags ?? [],
346879
348042
  error: result.error,
@@ -346887,6 +348050,10 @@ ${result2.logs}`);
346887
348050
  log.warn(`\u26A0 Rate-limit block detected - posting ERROR (not FAILED) to skip heal/quarantine`);
346888
348051
  }
346889
348052
  }
348053
+ if (runStatus === "FAILED" && classifyErrorMessage(runError ?? null) === "config_issue") {
348054
+ runStatus = "ERROR";
348055
+ log.warn(`\u26A0 CONFIG_ISSUE from native execution - posting ERROR (not FAILED) to skip heal/quarantine`);
348056
+ }
346890
348057
  await postResult(opts.serverUrl, headers, run2.runId, {
346891
348058
  status: runStatus,
346892
348059
  stepResults: result.stepResults,
@@ -346902,7 +348069,7 @@ ${result2.logs}`);
346902
348069
  if (result.passed) {
346903
348070
  log.ok(`PASSED (${duration}ms)${result.suspiciousPass ? ` \u26A0 suspicious-pass (viewport ${Math.round(result.suspiciousPass.emptyRatio * 100)}% blank)` : ""}`);
346904
348071
  } else if (runStatus === "ERROR") {
346905
- log.warn(`BLOCKED (${duration}ms) - rate-limit detected, not counted as failure`);
348072
+ log.warn(`BLOCKED (${duration}ms) - environmental block (rate-limit / config-issue), not counted as failure`);
346906
348073
  } else {
346907
348074
  log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
346908
348075
  if (result.logs) {
@@ -346934,6 +348101,12 @@ async function startRunner(opts) {
346934
348101
  process.env.AUTH_TOKEN = authToken;
346935
348102
  process.env.RUNNER_TOKEN = authToken;
346936
348103
  if (projectId) process.env.PROJECT_ID = projectId;
348104
+ (0, import_runner_core12.cleanupExpiredLocalAuthStateFiles)(getRunsDir());
348105
+ const authCleanupTimer = setInterval(
348106
+ () => (0, import_runner_core12.cleanupExpiredLocalAuthStateFiles)(getRunsDir()),
348107
+ import_runner_core12.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS
348108
+ );
348109
+ authCleanupTimer.unref?.();
346937
348110
  const headers = buildHeaders(authToken, projectId);
346938
348111
  console.log("");
346939
348112
  console.log(`${C2.bold}${C2.cyan}validate.qa Local Runner${C2.reset}`);
@@ -347042,7 +348215,7 @@ async function startRunner(opts) {
347042
348215
  capture: envInt("MAX_CAPTURE", DEFAULT_TYPE_LIMITS.capture),
347043
348216
  run: envInt("MAX_RUN", DEFAULT_TYPE_LIMITS.run)
347044
348217
  };
347045
- const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core12.createResourceGovernorOptions)({
348218
+ const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core13.createResourceGovernorOptions)({
347046
348219
  isCloud: IS_CLOUD,
347047
348220
  maxConcurrent: MAX_CONCURRENT,
347048
348221
  typeLimits: TYPE_LIMITS
@@ -347210,8 +348383,8 @@ async function startRunner(opts) {
347210
348383
  android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
347211
348384
  } : { web: true, ios: false, android: false };
347212
348385
  const bridge = enableMobile ? getBridgeState() : void 0;
347213
- const resourceStatus = (0, import_runner_core12.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
347214
- const processInventory = await (0, import_runner_core12.getBrowserProcessInventory)();
348386
+ const resourceStatus = (0, import_runner_core13.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
348387
+ const processInventory = await (0, import_runner_core13.getBrowserProcessInventory)();
347215
348388
  lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
347216
348389
  lastInventoryKinds.clear();
347217
348390
  for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
@@ -347241,7 +348414,7 @@ async function startRunner(opts) {
347241
348414
  resourceSnapshot: resourceStatus.snapshot,
347242
348415
  resourceGovernor: resourceStatus.governor,
347243
348416
  processInventory,
347244
- aiQueue: (0, import_runner_core12.getRunnerAIQueueStats)(),
348417
+ aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
347245
348418
  liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
347246
348419
  // Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
347247
348420
  // Cached & cheap — see getCachedDoctor above.
@@ -347352,7 +348525,7 @@ async function startRunner(opts) {
347352
348525
  let pidsWatchdogWarned = false;
347353
348526
  const pidsWatchdog = setInterval(() => {
347354
348527
  if (shuttingDown) return;
347355
- const snapshot = (0, import_runner_core12.getResourceSnapshot)();
348528
+ const snapshot = (0, import_runner_core13.getResourceSnapshot)();
347356
348529
  const pct = snapshot.pidsPct;
347357
348530
  if (pct == null || !Number.isFinite(pct)) return;
347358
348531
  if (pct >= PIDS_RECYCLE_PCT) {
@@ -347392,7 +348565,7 @@ async function startRunner(opts) {
347392
348565
  log.dim(` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
347393
348566
  continue;
347394
348567
  }
347395
- const admission = (0, import_runner_core12.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
348568
+ const admission = (0, import_runner_core13.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
347396
348569
  mode: runMode,
347397
348570
  activeRuns,
347398
348571
  activeByType: Object.fromEntries(activeByType),
@@ -347470,9 +348643,9 @@ async function startRunner(opts) {
347470
348643
  leasedDeviceId = freeDeviceId;
347471
348644
  leasedDeviceIds.set(freeDeviceId, run2.runId);
347472
348645
  }
347473
- let claimed = false;
348646
+ let claimEpoch = false;
347474
348647
  try {
347475
- claimed = await claimRun(serverUrl, headers, run2.runId);
348648
+ claimEpoch = await claimRun(serverUrl, headers, run2.runId);
347476
348649
  } catch (err) {
347477
348650
  if (leasedDeviceId) {
347478
348651
  leasedDeviceIds.delete(leasedDeviceId);
@@ -347480,7 +348653,7 @@ async function startRunner(opts) {
347480
348653
  }
347481
348654
  throw err;
347482
348655
  }
347483
- if (!claimed) {
348656
+ if (claimEpoch === false) {
347484
348657
  log.dim(` Skipped ${run2.runId} (already claimed)`);
347485
348658
  if (leasedDeviceId) {
347486
348659
  leasedDeviceIds.delete(leasedDeviceId);
@@ -347488,6 +348661,7 @@ async function startRunner(opts) {
347488
348661
  }
347489
348662
  continue;
347490
348663
  }
348664
+ run2.claimEpoch = claimEpoch;
347491
348665
  activeRuns++;
347492
348666
  incrementType(runMode);
347493
348667
  incrementProject(runProject);