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