@validate.qa/runner 1.0.43 → 1.0.45

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 +479 -59
  2. package/dist/cli.mjs +479 -59
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4779,6 +4779,204 @@ var init_origin_domain = __esm({
4779
4779
  }
4780
4780
  });
4781
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
+
4782
4980
  // ../shared/dist/voice-config.js
4783
4981
  function normalizeTtsVoiceId(voiceId) {
4784
4982
  if (!voiceId)
@@ -5053,8 +5251,16 @@ __export(dist_exports, {
5053
5251
  ADDON_DEFINITIONS: () => ADDON_DEFINITIONS,
5054
5252
  ADMIN_EMAIL_LIMITS: () => ADMIN_EMAIL_LIMITS,
5055
5253
  AI_CREDIT_MARGIN: () => AI_CREDIT_MARGIN,
5254
+ API_AUTH_HEADERS_ENV_KEY: () => API_AUTH_HEADERS_ENV_KEY,
5056
5255
  API_PREFIX: () => API_PREFIX,
5057
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,
5058
5264
  COST_PER_M: () => COST_PER_M,
5059
5265
  CREDIT_PACKS_CENTS: () => CREDIT_PACKS_CENTS,
5060
5266
  CREDIT_PRICES_CENTS: () => CREDIT_PRICES_CENTS,
@@ -5097,6 +5303,7 @@ __export(dist_exports, {
5097
5303
  STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
5098
5304
  STT_PROVIDER_OPTIONS: () => STT_PROVIDER_OPTIONS,
5099
5305
  SUPPORT_EMAIL: () => SUPPORT_EMAIL,
5306
+ TEST_USER_ENV_KEYS: () => TEST_USER_ENV_KEYS,
5100
5307
  TRANSIENT_CONTROL_ROLES: () => TRANSIENT_CONTROL_ROLES,
5101
5308
  TRIAL_DURATION_DAYS: () => TRIAL_DURATION_DAYS,
5102
5309
  TTS_MODEL_OPTIONS: () => TTS_MODEL_OPTIONS,
@@ -5106,10 +5313,12 @@ __export(dist_exports, {
5106
5313
  accumulateStatements: () => accumulateStatements,
5107
5314
  aiCostToCreditCents: () => aiCostToCreditCents,
5108
5315
  analyzeMobileSelectorQuality: () => analyzeMobileSelectorQuality,
5316
+ authTypeTag: () => authTypeTag,
5109
5317
  bugFoundQuarantineWhere: () => bugFoundQuarantineWhere,
5110
5318
  buildAppiumCode: () => buildAppiumCode,
5111
5319
  buildLaunchStep: () => buildLaunchStep,
5112
5320
  buildMobileStep: () => buildMobileStep,
5321
+ buildNeedleMatcher: () => buildNeedleMatcher,
5113
5322
  buildSandboxedEnv: () => buildSandboxedEnv,
5114
5323
  buildSanitizedAgentEnv: () => buildSanitizedAgentEnv,
5115
5324
  buildStepTarget: () => buildStepTarget,
@@ -5125,6 +5334,7 @@ __export(dist_exports, {
5125
5334
  controlSignature: () => controlSignature,
5126
5335
  countInnerTests: () => countInnerTests,
5127
5336
  decideHarPosted: () => decideHarPosted,
5337
+ deriveAuthPlan: () => deriveAuthPlan,
5128
5338
  deriveProjectKey: () => deriveProjectKey,
5129
5339
  detectCrossOriginHtmlAsJson: () => detectCrossOriginHtmlAsJson,
5130
5340
  detectsErrorBannerAssertion: () => detectsErrorBannerAssertion,
@@ -5192,6 +5402,8 @@ __export(dist_exports, {
5192
5402
  planHasBillingPortal: () => planHasBillingPortal,
5193
5403
  planRequiresCheckout: () => planRequiresCheckout,
5194
5404
  platformTag: () => platformTag,
5405
+ redactNeedlesDeep: () => redactNeedlesDeep,
5406
+ redactNeedlesText: () => redactNeedlesText,
5195
5407
  registrableDomain: () => registrableDomain,
5196
5408
  resolveCrossOriginConfigOverride: () => resolveCrossOriginConfigOverride,
5197
5409
  resolveMobileStepValuePlaceholders: () => resolveMobileStepValuePlaceholders,
@@ -5234,6 +5446,8 @@ var init_dist = __esm({
5234
5446
  init_agent_env();
5235
5447
  init_test_counting();
5236
5448
  init_origin_domain();
5449
+ init_auth_plan();
5450
+ init_redact_needles();
5237
5451
  init_voice_config();
5238
5452
  init_feature_flags();
5239
5453
  }
@@ -52401,11 +52615,11 @@ var require_leac = __commonJS({
52401
52615
  if (r.str.length === 0) {
52402
52616
  throw new Error(`Rule #${i} ("${r.name}") has empty "str" property, which is not allowed.`);
52403
52617
  }
52404
- return new RegExp(escapeRegExp(r.str), "y");
52618
+ return new RegExp(escapeRegExp2(r.str), "y");
52405
52619
  }
52406
- return new RegExp(escapeRegExp(r.name), "y");
52620
+ return new RegExp(escapeRegExp2(r.name), "y");
52407
52621
  }
52408
- function escapeRegExp(str) {
52622
+ function escapeRegExp2(str) {
52409
52623
  return str.replace(/[-[\]{}()*+!<=:?./\\^$|#\s,]/g, "\\$&");
52410
52624
  }
52411
52625
  function toSticky(re) {
@@ -68378,7 +68592,7 @@ var require_discover_ai_planner = __commonJS({
68378
68592
  "../runner-core/dist/services/discover-ai-planner.js"(exports2) {
68379
68593
  "use strict";
68380
68594
  Object.defineProperty(exports2, "__esModule", { value: true });
68381
- exports2.SCENARIO_SYSTEM_PROMPT = void 0;
68595
+ exports2.PLANNER_STRENGTHEN_GATE_PROMPT = exports2.SCENARIO_SYSTEM_PROMPT = void 0;
68382
68596
  exports2.getAIPlannerMaxTokens = getAIPlannerMaxTokens;
68383
68597
  exports2.getSnapshotConfidence = getSnapshotConfidence;
68384
68598
  exports2.compressSiteMap = compressSiteMap;
@@ -68961,7 +69175,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
68961
69175
  4. **Multi-page journeys** - only when transitions were actually observed
68962
69176
  5. **Bug reports** - for observed 500 errors, crashes, or broken features
68963
69177
  6. **Steps are human-readable** - "Fill the email field", not "page.getByLabel('Email').fill()"
68964
- 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.
68965
69179
  8. **Auth-aware** - mark scenarios that need login. Use process.env.TEST_USER_EMAIL / PASSWORD
68966
69180
  9. **No duplicates** - check existing test names before adding
68967
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.
@@ -68974,7 +69188,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
68974
69188
  Example - BAD happy path: \`["Navigate to /feature", "Click Create", "Verify dialog opens"]\` \u2192 only tests the trigger.
68975
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"]\`
68976
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"]\`
68977
- 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.
68978
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.
68979
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>."
68980
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.
@@ -69037,6 +69251,9 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
69037
69251
  Citing observed labels in \`expectedOutcomes\` is fine \u2014 that's asserting on stable product signals, not implementation specifics.
69038
69252
 
69039
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):
69040
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)
69041
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)
69042
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.
@@ -69066,11 +69283,11 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
69066
69283
  { "action": "Fill the email field with test credentials" },
69067
69284
  { "action": "Fill the password field with test credentials" },
69068
69285
  { "action": "Click the Sign In button" },
69069
- { "action": "Open the observed authenticated account or user menu" }
69286
+ { "action": "After reaching the observed post-login page, reload it" }
69070
69287
  ],
69071
69288
  "expectedOutcomes": [
69072
- { "type": "url", "description": "User reaches /dashboard after signing in", "reliability": "high" },
69073
- { "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" }
69074
69291
  ],
69075
69292
  "targetPages": ["/auth/login", "/dashboard"],
69076
69293
  "prerequisites": []
@@ -69100,6 +69317,46 @@ Each entry is exactly one of:
69100
69317
  3 = medium (navigation, secondary features)
69101
69318
  4 = low (edge cases, cosmetic)
69102
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.`;
69103
69360
  function coerceRawPrerequisites(raw) {
69104
69361
  if (!Array.isArray(raw))
69105
69362
  return [];
@@ -69883,6 +70140,95 @@ Each entry is exactly one of:
69883
70140
  }
69884
70141
  }
69885
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
+ }
69886
70232
  const totalFinal = scenarios.length + coverageGaps.length;
69887
70233
  const rejectionRateFinal = totalFinal > 0 ? coverageGaps.length / totalFinal : 0;
69888
70234
  if (rejectionRateFinal > 0.5 && totalFinal >= 3) {
@@ -73662,7 +74008,7 @@ var require_mobile_heal_postprocess = __commonJS({
73662
74008
  // OpenAI-style secret key
73663
74009
  ];
73664
74010
  var EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
73665
- function escapeRegExp(value) {
74011
+ function escapeRegExp2(value) {
73666
74012
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
73667
74013
  }
73668
74014
  function collectSecretVars(testVariables, minLen, keyRe) {
@@ -73681,7 +74027,7 @@ var require_mobile_heal_postprocess = __commonJS({
73681
74027
  return text;
73682
74028
  let out = text;
73683
74029
  for (const { key, value } of collectSecretVars(testVariables, MIN_REDACT_LEN, REDACT_KEY_RE)) {
73684
- out = out.replace(new RegExp(escapeRegExp(value), "g"), `{{${key}}}`);
74030
+ out = out.replace(new RegExp(escapeRegExp2(value), "g"), `{{${key}}}`);
73685
74031
  }
73686
74032
  for (const source of TOKEN_SOURCES) {
73687
74033
  out = out.replace(new RegExp(source, "g"), "[REDACTED]");
@@ -76081,7 +76427,7 @@ var require_api_auth_headers = __commonJS({
76081
76427
  "../runner-core/dist/services/api-auth-headers.js"(exports2) {
76082
76428
  "use strict";
76083
76429
  Object.defineProperty(exports2, "__esModule", { value: true });
76084
- exports2.parseApiAuthHeaders = parseApiAuthHeaders;
76430
+ exports2.parseApiAuthHeaders = parseApiAuthHeaders2;
76085
76431
  exports2.resolveApiAuthHeaders = resolveApiAuthHeaders;
76086
76432
  exports2.isAppOriginRequest = isAppOriginRequest;
76087
76433
  exports2.buildOriginScopedApiFixtures = buildOriginScopedApiFixtures;
@@ -76094,7 +76440,7 @@ var require_api_auth_headers = __commonJS({
76094
76440
  }
76095
76441
  return false;
76096
76442
  }
76097
- function parseApiAuthHeaders(raw) {
76443
+ function parseApiAuthHeaders2(raw) {
76098
76444
  if (!raw || typeof raw !== "string")
76099
76445
  return void 0;
76100
76446
  const trimmed = raw.trim();
@@ -76118,9 +76464,9 @@ var require_api_auth_headers = __commonJS({
76118
76464
  function resolveApiAuthHeaders(input) {
76119
76465
  if (input.explicit)
76120
76466
  return input.explicit;
76121
- if (!input.isApiTest)
76467
+ if (input.standingHeadersMode === "none")
76122
76468
  return void 0;
76123
- return parseApiAuthHeaders(input.apiAuthHeadersVar);
76469
+ return parseApiAuthHeaders2(input.apiAuthHeadersVar);
76124
76470
  }
76125
76471
  function isAppOriginRequest(url, baseUrl) {
76126
76472
  if (typeof url !== "string" || !baseUrl)
@@ -76387,7 +76733,7 @@ var require_playwright_native = __commonJS({
76387
76733
  var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
76388
76734
  var PW_CLI = require.resolve("@playwright/test/cli");
76389
76735
  async function executeTestViaNativePlaywright2(options) {
76390
- const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
76736
+ const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, authStandingMode, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
76391
76737
  const logs2 = [];
76392
76738
  const screenshots = [];
76393
76739
  logs2.push(`\u{1F3AD} Native Playwright starting`);
@@ -76397,16 +76743,19 @@ var require_playwright_native = __commonJS({
76397
76743
  logs2.push("\u26A0\uFE0F WARNING: baseUrl is empty - test will likely fail with navigation errors");
76398
76744
  }
76399
76745
  let runDir = null;
76746
+ let standingNeedleMatcher = null;
76400
76747
  let callbackProxy;
76401
76748
  try {
76402
76749
  runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
76403
76750
  await (0, promises_1.mkdir)(runDir, { recursive: true });
76404
76751
  const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
76752
+ const effectiveStandingMode = isApiTest2 ? authStandingMode ?? "legacy-if-configured" : "none";
76405
76753
  const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
76406
76754
  explicit: extraHttpHeaders,
76407
- isApiTest: isApiTest2,
76755
+ standingHeadersMode: effectiveStandingMode,
76408
76756
  apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
76409
76757
  });
76758
+ standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)(resolvedExtraHeaders ? Object.values(resolvedExtraHeaders) : []);
76410
76759
  const cssDisableSetup = `
76411
76760
  const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
76412
76761
  let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
@@ -76601,6 +76950,18 @@ test.afterEach(async ({ page }, testInfo) => {
76601
76950
  logs2.push("\u26A0 Could not find @playwright/test import - animation disabling NOT injected");
76602
76951
  }
76603
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
+ }
76604
76965
  let instrumentedCode;
76605
76966
  if (isApiTest2) {
76606
76967
  instrumentedCode = useScopedApiHeaders ? playwrightCode.replace(importMatch[0], importMatch[0].replace(/from\s+['"]@playwright\/test['"]/, "from './qa-api-fixtures'")) : playwrightCode;
@@ -76624,7 +76985,7 @@ test.afterEach(async ({ page }, testInfo) => {
76624
76985
  const absHarPath = wantHar ? (0, path_1.join)(runDir, "session.har").replace(/\\/g, "/") : null;
76625
76986
  const recordHarLine = absHarPath ? `recordHar: { path: ${JSON.stringify(absHarPath)}, mode: 'full' },` : "";
76626
76987
  const storageStateLine = storageStatePath ? `storageState: ${JSON.stringify(storageStatePath.replace(/\\/g, "/"))},` : "";
76627
- const extraHeadersLine = resolvedExtraHeaders && !useScopedApiHeaders && Object.keys(resolvedExtraHeaders).length > 0 ? `extraHTTPHeaders: ${JSON.stringify(resolvedExtraHeaders)},` : "";
76988
+ const extraHeadersLine = "";
76628
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'],";
76629
76990
  const config = `
76630
76991
  // Plain object export - no require('@playwright/test') needed.
@@ -76762,7 +77123,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76762
77123
  logs2.push(`\u26A0 HAR parse skipped: ${msg.slice(0, 200)}`);
76763
77124
  }
76764
77125
  }
76765
- return {
77126
+ const nativeResult = {
76766
77127
  passed,
76767
77128
  stepResults,
76768
77129
  screenshots,
@@ -76771,19 +77132,24 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76771
77132
  harApiCalls,
76772
77133
  logs: logs2.join("\n"),
76773
77134
  error: passed ? void 0 : error2,
76774
- suspiciousPass
77135
+ suspiciousPass,
77136
+ authDelivery
76775
77137
  };
77138
+ if (!standingNeedleMatcher)
77139
+ return nativeResult;
77140
+ return (0, shared_1.redactNeedlesDeep)(nativeResult, standingNeedleMatcher);
76776
77141
  } catch (fatalErr) {
76777
77142
  const msg = fatalErr instanceof Error ? fatalErr.message : String(fatalErr);
76778
77143
  logs2.push(`
76779
77144
  \u{1F4A5} Fatal error: ${msg}`);
76780
- return {
77145
+ const fatalResult = {
76781
77146
  passed: false,
76782
77147
  stepResults: [],
76783
77148
  screenshots,
76784
77149
  logs: logs2.join("\n"),
76785
77150
  error: msg
76786
77151
  };
77152
+ return standingNeedleMatcher ? (0, shared_1.redactNeedlesDeep)(fatalResult, standingNeedleMatcher) : fatalResult;
76787
77153
  } finally {
76788
77154
  await callbackProxy?.close().catch(() => {
76789
77155
  });
@@ -85887,7 +86253,7 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
85887
86253
  }
85888
86254
  return result;
85889
86255
  }
85890
- function escapeRegExp(value) {
86256
+ function escapeRegExp2(value) {
85891
86257
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
85892
86258
  }
85893
86259
  function buildSensitiveEnvReplacements(testVariables) {
@@ -85909,7 +86275,7 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
85909
86275
  for (const replacement of buildSensitiveEnvReplacements(testVariables)) {
85910
86276
  if (replacement.value.includes("\n") || replacement.value.includes("\r"))
85911
86277
  continue;
85912
- const literalPattern = new RegExp(`(['"\`])${escapeRegExp(replacement.value)}\\1`, "g");
86278
+ const literalPattern = new RegExp(`(['"\`])${escapeRegExp2(replacement.value)}\\1`, "g");
85913
86279
  result = result.replace(literalPattern, replacement.envRef);
85914
86280
  }
85915
86281
  return result;
@@ -255165,7 +255531,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
255165
255531
  }
255166
255532
  }
255167
255533
  return result;
255168
- function escapeRegExp(str) {
255534
+ function escapeRegExp2(str) {
255169
255535
  return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
255170
255536
  }
255171
255537
  function getTodoCommentsRegExp() {
@@ -255173,7 +255539,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
255173
255539
  const multiLineCommentStart = /(?:\/\*+\s*)/.source;
255174
255540
  const anyNumberOfSpacesAndAsterisksAtStartOfLine = /(?:^(?:\s|\*)*)/.source;
255175
255541
  const preamble = "(" + anyNumberOfSpacesAndAsterisksAtStartOfLine + "|" + singleLineCommentStart + "|" + multiLineCommentStart + ")";
255176
- const literals = "(?:" + map2(descriptors, (d) => "(" + escapeRegExp(d.text) + ")").join("|") + ")";
255542
+ const literals = "(?:" + map2(descriptors, (d) => "(" + escapeRegExp2(d.text) + ")").join("|") + ")";
255177
255543
  const endOfLineOrEndOfComment = /(?:$|\*\/)/.source;
255178
255544
  const messageRemainder = /(?:.*?)/.source;
255179
255545
  const messagePortion = "(" + literals + messageRemainder + ")";
@@ -303433,7 +303799,7 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
303433
303799
  confidence: "low"
303434
303800
  };
303435
303801
  }
303436
- function escapeRegExp(s) {
303802
+ function escapeRegExp2(s) {
303437
303803
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
303438
303804
  }
303439
303805
  function tryDeterministicStrictModeFix(code, error2) {
@@ -303456,10 +303822,10 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
303456
303822
  const [, , role, , name, restOfOptions] = violated;
303457
303823
  if (/exact\s*:\s*true/.test(restOfOptions))
303458
303824
  return null;
303459
- 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*\\)`);
303460
303826
  if (!akaPattern.test(error2))
303461
303827
  return null;
303462
- 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");
303463
303829
  let modified = false;
303464
303830
  const patched = code.replace(codePattern, (full, prefix, _q1, _q2, suffix) => {
303465
303831
  if (/exact\s*:\s*true/.test(suffix))
@@ -303476,11 +303842,11 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
303476
303842
  const [, , text, optionsBlock] = violated;
303477
303843
  if (optionsBlock && /exact\s*:\s*true/.test(optionsBlock))
303478
303844
  return null;
303479
- 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*\\)`);
303480
303846
  if (!akaPattern.test(error2))
303481
303847
  return null;
303482
- const noOptsPattern = new RegExp(`getByText\\(\\s*(['"\`])${escapeRegExp(text)}\\1\\s*\\)`, "g");
303483
- 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");
303484
303850
  let modified = false;
303485
303851
  let patched = code.replace(noOptsPattern, (_full, q) => {
303486
303852
  modified = true;
@@ -315953,6 +316319,8 @@ var require_auth_capture_run = __commonJS({
315953
316319
  "../runner-core/dist/services/auth-capture-run.js"(exports2) {
315954
316320
  "use strict";
315955
316321
  Object.defineProperty(exports2, "__esModule", { value: true });
316322
+ exports2.filterLoginCaptureCredentials = filterLoginCaptureCredentials;
316323
+ exports2.buildLoginCaptureCredentials = buildLoginCaptureCredentials;
315956
316324
  exports2.getLoginCaptureMaxIterations = getLoginCaptureMaxIterations;
315957
316325
  exports2.harvestAuthProofLive = harvestAuthProofLive;
315958
316326
  exports2.findCredentialLiteralLeaks = findCredentialLiteralLeaks;
@@ -315968,6 +316336,26 @@ var require_auth_capture_run = __commonJS({
315968
316336
  var playwright_native_js_1 = require_playwright_native();
315969
316337
  var scrub_credentials_js_1 = require_scrub_credentials();
315970
316338
  var credential_tools_js_1 = require_credential_tools();
316339
+ var LOGIN_CAPTURE_CONTROL_VARIABLES = /* @__PURE__ */ new Set([
316340
+ "BASE_URL",
316341
+ "DISCOVERY_SHOULD_REGISTER",
316342
+ "DISCOVERY_AUTH_MODE"
316343
+ ]);
316344
+ function filterLoginCaptureCredentials(variables) {
316345
+ const credentials = {};
316346
+ for (const [key, value] of Object.entries(variables)) {
316347
+ if ((0, auth_recipe_js_1.isReservedRunnerEnvVar)(key) || LOGIN_CAPTURE_CONTROL_VARIABLES.has(key))
316348
+ continue;
316349
+ credentials[key] = value;
316350
+ }
316351
+ return credentials;
316352
+ }
316353
+ function buildLoginCaptureCredentials(variableContext) {
316354
+ return filterLoginCaptureCredentials({
316355
+ ...variableContext.publicTestVariables,
316356
+ ...variableContext.allTestVariables
316357
+ });
316358
+ }
315971
316359
  var DEFAULT_MAX_LOGIN_CAPTURE_ITERATIONS = 12;
315972
316360
  function readPositiveIntEnv2(env, name, fallback) {
315973
316361
  const raw = env[name]?.trim();
@@ -316175,7 +316563,8 @@ ${keyLines || "- (none provided)"}
316175
316563
  return false;
316176
316564
  }
316177
316565
  async function runLoginCaptureRun(args) {
316178
- const { loginBrowser, baseUrl, credentials, projectId, model, onAudit, logs: logs2 } = args;
316566
+ const { loginBrowser, baseUrl, projectId, model, onAudit, logs: logs2 } = args;
316567
+ const credentials = filterLoginCaptureCredentials(args.credentials);
316179
316568
  if ((0, agentic_cli_constants_js_1.getAgenticEngine)(model) !== "native") {
316180
316569
  logs2(` auth-capture skipped: engine for ${model} is not native (V1 is native-engine-only)`);
316181
316570
  return null;
@@ -321437,9 +321826,9 @@ var require_tool_ledger = __commonJS({
321437
321826
  const ledgerPath = (0, node_path_1.join)(runDir, "discovery-events.jsonl");
321438
321827
  let checkpointIndex = 0;
321439
321828
  let headerWritten = false;
321440
- const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
321829
+ const escapeRegExp2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
321441
321830
  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]) => ({
321442
- re: new RegExp(escapeRegExp(value), "g"),
321831
+ re: new RegExp(escapeRegExp2(value), "g"),
321443
321832
  replacement: `[REDACTED_${envName}]`
321444
321833
  }));
321445
321834
  const scrubCredentials = (text) => {
@@ -323314,7 +323703,7 @@ Respond with ONLY valid JSON:
323314
323703
  pushLog("auth-prologue: AUTH_RECIPE_STAMP on + native engine, but no login-browser factory supplied (runner stage 2b) \u2014 skipping producer");
323315
323704
  return null;
323316
323705
  }
323317
- const credentials = ctx.variableContext.allTestVariables ?? ctx.variableContext.publicTestVariables;
323706
+ const credentials = (0, auth_capture_run_js_1.buildLoginCaptureCredentials)(ctx.variableContext);
323318
323707
  const run2 = (async () => {
323319
323708
  let lease = null;
323320
323709
  try {
@@ -323599,6 +323988,7 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
323599
323988
  testBudget
323600
323989
  }, {
323601
323990
  modelOverride: ctx.planModel ?? ctx.generateModel,
323991
+ gateModelOverride: ctx.planGateModel,
323602
323992
  onAICall
323603
323993
  });
323604
323994
  logs2.push(...planResult.logs);
@@ -324177,6 +324567,7 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
324177
324567
  testBudget: featureTestBudget
324178
324568
  }, {
324179
324569
  modelOverride: ctx.planModel ?? ctx.generateModel,
324570
+ gateModelOverride: ctx.planGateModel,
324180
324571
  onAICall
324181
324572
  });
324182
324573
  logs2.push(...planResult.logs);
@@ -337611,9 +338002,9 @@ var require_dist2 = __commonJS({
337611
338002
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
337612
338003
  };
337613
338004
  Object.defineProperty(exports2, "__esModule", { value: true });
337614
- exports2.MAX_ARCHIVE_BYTES = exports2.isUnsafeEntryName = exports2.extractZipSafely = exports2.downloadArchiveCapped = exports2.detectArchiveKind = exports2.acquireRepoForImport = exports2.runFrameworkImportOnRunner = exports2.getAgenticEngine = exports2.resolveRefFromSnapshot = exports2.replayAuthRecipeIntoBrowser = exports2.runRegisterCaptureRun = exports2.runLoginCaptureRun = exports2.isReservedRunnerEnvVar = exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = exports2.buildHealAuthStamp = exports2.extractAuthRegion = exports2.upsertAuthBlock = exports2.buildAuthRecipe = exports2.commandToReplayStep = exports2.buildReplayPlan = exports2.absoluteRoute = exports2.renderReplaySettleArrow = exports2.renderReplayClickArrow = exports2.renderAuthFn = exports2.canonicalizeRoute = exports2.executeGrokPerBatchHeal = exports2.executeGrokPerTestHeal = exports2.headerVal = exports2.classifyAuthType = exports2.isTextExcerptable = exports2.isStaticAssetByExt = exports2.isApiCall = exports2.buildApiCallSummary = exports2.computeMobileHealOutcome = exports2.detectMobileStepSecretLeak = exports2.redactMobileSecretsFromText = exports2.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports2.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = exports2.executeMobileLiveHeal = exports2.executeMobileTestWithHealing = exports2.executeMobileScriptDrivenHeal = exports2.prepareMobileScenarioAppState = exports2.runMobileGeneratePhase = exports2.runMobileAIPlanner = exports2.buildMobileSiteMap = exports2.formatMobileEvidenceForPlanner = exports2.MOBILE_SCENARIO_SYSTEM_PROMPT = exports2.MOBILE_SURVEY_SYSTEM_PROMPT = exports2.runMobileExplorePhase = void 0;
337615
- exports2.modelSupportsVision = exports2.decideCanvasStrategy = exports2.decideAppRenderType = exports2.detectCanvasApp = exports2.detectRateLimitBlock = exports2.isSafeCanonicalRedirect = exports2.resolveBaseUrlPreflight = exports2.checkBaseUrlRedirect = exports2.stripJsonFences = exports2.scrubCredentials = exports2.withAIRetry = exports2.snapshotMatchingChildren = exports2.findNewChromiumChildPid = exports2.killProcessTree = exports2.gracefulCloseWithTreeKill = exports2.liveBrowserRegistry = exports2.REPO_SWITCH_REMOVED_FRACTION = exports2.MAX_MANIFEST_FILES = exports2.toDiffSummary = exports2.isLikelyTestFile = exports2.sanitizeRepoRelPath = exports2.sha256Hex = exports2.hashFiles = exports2.enumerateTestFiles = exports2.buildNextManifest = exports2.detectRepoSwitch = exports2.classifyImportDiff = exports2.parseImportManifest = exports2.REPO_ANALYSIS_SCHEMA_VERSION = exports2.validateSourceEndpoints = exports2.validateAuthoredTest = exports2.validateRepoMapFile = exports2.validateRepoMemoryFile = exports2.buildTestingStandardsSection = exports2.buildRepoAuthorSystemRules = exports2.buildRepoAuthorKickoff = exports2.buildRepoAuthorContractMd = exports2.buildRepoMapSystemRules = exports2.buildRepoMapKickoff = exports2.buildRepoMapContractMd = exports2.buildRepoMemorySystemRules = exports2.buildRepoMemoryKickoff = exports2.buildRepoMemoryContractMd = exports2.runRepoAnalysisOnRunner = exports2.IMPORT_SCHEMA_VERSION = exports2.validateImportResultFile = exports2.validateImportedTest = exports2.buildImportSystemRules = exports2.buildImportKickoff = exports2.buildImportContractMd = void 0;
337616
- exports2.UNSUPPORTED_RENDERER_PREFIX = exports2.UnsupportedRendererError = exports2.buildCanvasModeAuthoringAddendum = exports2.buildUnsupportedRendererMessage = void 0;
338005
+ exports2.extractZipSafely = exports2.downloadArchiveCapped = exports2.detectArchiveKind = exports2.acquireRepoForImport = exports2.runFrameworkImportOnRunner = exports2.getAgenticEngine = exports2.resolveRefFromSnapshot = exports2.replayAuthRecipeIntoBrowser = exports2.runRegisterCaptureRun = exports2.runLoginCaptureRun = exports2.filterLoginCaptureCredentials = exports2.buildLoginCaptureCredentials = exports2.isReservedRunnerEnvVar = exports2.RESERVED_RUNNER_ENV_VARS = exports2.AUTH_MARKER_VERSION = exports2.buildHealAuthStamp = exports2.extractAuthRegion = exports2.upsertAuthBlock = exports2.buildAuthRecipe = exports2.commandToReplayStep = exports2.buildReplayPlan = exports2.absoluteRoute = exports2.renderReplaySettleArrow = exports2.renderReplayClickArrow = exports2.renderAuthFn = exports2.canonicalizeRoute = exports2.executeGrokPerBatchHeal = exports2.executeGrokPerTestHeal = exports2.headerVal = exports2.classifyAuthType = exports2.isTextExcerptable = exports2.isStaticAssetByExt = exports2.isApiCall = exports2.buildApiCallSummary = exports2.computeMobileHealOutcome = exports2.detectMobileStepSecretLeak = exports2.redactMobileSecretsFromText = exports2.MOBILE_LIVE_HEAL_SYSTEM_PROMPT = exports2.MOBILE_SCRIPT_HEAL_SYSTEM_PROMPT = exports2.executeMobileLiveHeal = exports2.executeMobileTestWithHealing = exports2.executeMobileScriptDrivenHeal = exports2.prepareMobileScenarioAppState = exports2.runMobileGeneratePhase = exports2.runMobileAIPlanner = exports2.buildMobileSiteMap = exports2.formatMobileEvidenceForPlanner = exports2.MOBILE_SCENARIO_SYSTEM_PROMPT = exports2.MOBILE_SURVEY_SYSTEM_PROMPT = exports2.runMobileExplorePhase = void 0;
338006
+ exports2.decideAppRenderType = exports2.detectCanvasApp = exports2.detectRateLimitBlock = exports2.isSafeCanonicalRedirect = exports2.resolveBaseUrlPreflight = exports2.checkBaseUrlRedirect = exports2.stripJsonFences = exports2.scrubCredentials = exports2.withAIRetry = exports2.snapshotMatchingChildren = exports2.findNewChromiumChildPid = exports2.killProcessTree = exports2.gracefulCloseWithTreeKill = exports2.liveBrowserRegistry = exports2.REPO_SWITCH_REMOVED_FRACTION = exports2.MAX_MANIFEST_FILES = exports2.toDiffSummary = exports2.isLikelyTestFile = exports2.sanitizeRepoRelPath = exports2.sha256Hex = exports2.hashFiles = exports2.enumerateTestFiles = exports2.buildNextManifest = exports2.detectRepoSwitch = exports2.classifyImportDiff = exports2.parseImportManifest = exports2.REPO_ANALYSIS_SCHEMA_VERSION = exports2.validateSourceEndpoints = exports2.validateAuthoredTest = exports2.validateRepoMapFile = exports2.validateRepoMemoryFile = exports2.buildTestingStandardsSection = exports2.buildRepoAuthorSystemRules = exports2.buildRepoAuthorKickoff = exports2.buildRepoAuthorContractMd = exports2.buildRepoMapSystemRules = exports2.buildRepoMapKickoff = exports2.buildRepoMapContractMd = exports2.buildRepoMemorySystemRules = exports2.buildRepoMemoryKickoff = exports2.buildRepoMemoryContractMd = exports2.runRepoAnalysisOnRunner = exports2.IMPORT_SCHEMA_VERSION = exports2.validateImportResultFile = exports2.validateImportedTest = exports2.buildImportSystemRules = exports2.buildImportKickoff = exports2.buildImportContractMd = exports2.MAX_ARCHIVE_BYTES = exports2.isUnsafeEntryName = void 0;
338007
+ exports2.UNSUPPORTED_RENDERER_PREFIX = exports2.UnsupportedRendererError = exports2.buildCanvasModeAuthoringAddendum = exports2.buildUnsupportedRendererMessage = exports2.modelSupportsVision = exports2.decideCanvasStrategy = void 0;
337617
338008
  __exportStar(require_ai_provider(), exports2);
337618
338009
  __exportStar(require_mobile_types(), exports2);
337619
338010
  __exportStar(require_mobile_source_parser(), exports2);
@@ -337762,6 +338153,12 @@ var require_dist2 = __commonJS({
337762
338153
  return auth_recipe_js_1.isReservedRunnerEnvVar;
337763
338154
  } });
337764
338155
  var auth_capture_run_js_1 = require_auth_capture_run();
338156
+ Object.defineProperty(exports2, "buildLoginCaptureCredentials", { enumerable: true, get: function() {
338157
+ return auth_capture_run_js_1.buildLoginCaptureCredentials;
338158
+ } });
338159
+ Object.defineProperty(exports2, "filterLoginCaptureCredentials", { enumerable: true, get: function() {
338160
+ return auth_capture_run_js_1.filterLoginCaptureCredentials;
338161
+ } });
337765
338162
  Object.defineProperty(exports2, "runLoginCaptureRun", { enumerable: true, get: function() {
337766
338163
  return auth_capture_run_js_1.runLoginCaptureRun;
337767
338164
  } });
@@ -337988,7 +338385,7 @@ var require_package4 = __commonJS({
337988
338385
  "package.json"(exports2, module2) {
337989
338386
  module2.exports = {
337990
338387
  name: "@validate.qa/runner",
337991
- version: "1.0.43",
338388
+ version: "1.0.45",
337992
338389
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
337993
338390
  bin: {
337994
338391
  "validate-runner": "dist/cli.js"
@@ -338106,32 +338503,24 @@ var import_runner_core12 = __toESM(require_dist2());
338106
338503
 
338107
338504
  // src/services/auth-state.ts
338108
338505
  init_dist();
338109
- var INLINE_LOGIN_TAG = "@inline-login";
338110
338506
  var GUARDED_LOGIN_TAG = "@auth-guarded";
338111
338507
  function sharedAuthInjectEnabled() {
338112
338508
  return isSharedAuthInjectEnabled();
338113
338509
  }
338114
- function hasGeneratedAuthGuard(playwrightCode) {
338510
+ function hasGeneratedAuthGuard2(playwrightCode) {
338115
338511
  if (!playwrightCode) return false;
338116
338512
  return /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
338117
338513
  }
338118
338514
  function shouldCaptureAuthState(tags, playwrightCode) {
338119
- return sharedAuthInjectEnabled() && Array.isArray(tags) && tags.includes("@needs-auth") && tags.includes(GUARDED_LOGIN_TAG) && !tags.includes("@auth") && hasGeneratedAuthGuard(playwrightCode);
338120
- }
338121
- function hasInlineLoginSteps(playwrightCode) {
338122
- if (!playwrightCode) return false;
338123
- return /process\.env\.TEST_USER_EMAIL!?/.test(playwrightCode) && /process\.env\.TEST_USER_PASSWORD!?/.test(playwrightCode);
338515
+ return sharedAuthInjectEnabled() && Array.isArray(tags) && tags.includes("@needs-auth") && tags.includes(GUARDED_LOGIN_TAG) && !tags.includes("@auth") && hasGeneratedAuthGuard2(playwrightCode);
338124
338516
  }
338125
338517
  function shouldUseAuthState(tags, playwrightCode) {
338126
338518
  if (!sharedAuthInjectEnabled()) return false;
338127
338519
  if (!Array.isArray(tags)) return false;
338128
- if (tags.includes("@auth")) return false;
338129
- if (tags.includes("@api")) return true;
338130
- if (!tags.includes("@needs-auth")) return false;
338131
- if (tags.includes(GUARDED_LOGIN_TAG) && hasGeneratedAuthGuard(playwrightCode)) return true;
338132
- const isGeneratedVariant = tags.includes("@edge") || tags.includes("@bug");
338133
- const hasLegacyInlineLogin = tags.includes("@discovered") && !isGeneratedVariant && hasInlineLoginSteps(playwrightCode);
338134
- return !tags.includes(INLINE_LOGIN_TAG) && !hasLegacyInlineLogin;
338520
+ return authPlanFor(tags, playwrightCode).needsCookieState;
338521
+ }
338522
+ function authPlanFor(tags, playwrightCode) {
338523
+ return deriveAuthPlan(tags, playwrightCode, { sharedAuthInjectEnabled: sharedAuthInjectEnabled() });
338135
338524
  }
338136
338525
 
338137
338526
  // src/version-check.ts
@@ -345527,7 +345916,18 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
345527
345916
  requestLiveBrowserHeartbeat();
345528
345917
  }, 5e3);
345529
345918
  liveBrowserHeartbeatInterval.unref?.();
345919
+ const authPlan = authPlanFor(run2.tags, run2.playwrightCode);
345920
+ const runAuthCachePath = (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
345921
+ projectId: run2.projectId || opts.projectId || "default",
345922
+ serverUrl: opts.serverUrl,
345923
+ baseUrl: run2.baseUrl,
345924
+ testVariables: run2.testVariables,
345925
+ authToken: opts.authToken
345926
+ }, run2.runId).cachePath;
345927
+ const wrapperStorageStatePath = authPlan.needsCookieState && runMode !== "auth-setup" ? (0, import_runner_core12.getValidLocalAuthState)(runAuthCachePath) : void 0;
345530
345928
  const runNativeWithLiveBrowser = (nativeOptions) => (0, import_runner_core.executeTestViaNativePlaywright)({
345929
+ authStandingMode: authPlan.standingHeadersMode,
345930
+ storageStatePath: wrapperStorageStatePath,
345531
345931
  ...nativeOptions,
345532
345932
  liveBrowserHandle,
345533
345933
  onLiveBrowserSnapshot: requestLiveBrowserHeartbeat
@@ -345555,11 +345955,24 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
345555
345955
  if (!run2.baseUrl) {
345556
345956
  preflightError = "Pre-flight failed: Missing BASE_URL. Configure it in Test Data \u2192 Test Variables.";
345557
345957
  } else {
345558
- const needsAuth = Array.isArray(run2.tags) && run2.tags.some((t) => t === "@auth" || t === "@needs-auth" || t === "@api");
345559
- if (needsAuth && !run2.testVariables?.["TEST_USER_EMAIL"]) {
345560
- preflightError = "Pre-flight failed: Missing TEST_USER_EMAIL. Tests tagged @auth, @needs-auth, or @api require login credentials.";
345561
- } else if (needsAuth && !run2.testVariables?.["TEST_USER_PASSWORD"]) {
345562
- preflightError = "Pre-flight failed: Missing TEST_USER_PASSWORD. Tests tagged @auth, @needs-auth, or @api require login credentials.";
345958
+ const validStanding = (0, import_runner_core13.parseApiAuthHeaders)(run2.testVariables?.["API_AUTH_HEADERS"]) !== void 0;
345959
+ if (authPlan.configurationError) {
345960
+ preflightError = `Pre-flight failed: ${authPlan.configurationError}. Fix the test's auth-type tag before running.`;
345961
+ } else if (authPlan.needsStandingHeaders && !validStanding) {
345962
+ preflightError = "Pre-flight failed: This @auth-standing @api test requires a valid API_AUTH_HEADERS JSON object (header name \u2192 value).";
345963
+ } else {
345964
+ const legacyHeaderBypass = authPlan.source === "legacy" && authPlan.standingHeadersMode === "legacy-if-configured" && validStanding;
345965
+ const code = run2.playwrightCode ?? "";
345966
+ const codeProvided = code.trim().length > 0;
345967
+ const codeUsesCreds = codeProvided && /TEST_USER_EMAIL|TEST_USER_PASSWORD|__vqaLogin/.test(code);
345968
+ const tags = Array.isArray(run2.tags) ? run2.tags : [];
345969
+ const isHallucinationShape = codeProvided && !codeUsesCreds && tags.includes("@inline-login") && !tags.includes("@api");
345970
+ const requireCreds = authPlan.needsLoginCreds && !legacyHeaderBypass && !isHallucinationShape;
345971
+ if (requireCreds && !run2.testVariables?.["TEST_USER_EMAIL"]?.trim()) {
345972
+ 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.";
345973
+ } else if (requireCreds && !run2.testVariables?.["TEST_USER_PASSWORD"]?.trim()) {
345974
+ 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.";
345975
+ }
345563
345976
  }
345564
345977
  }
345565
345978
  if (preflightError) {
@@ -346168,10 +346581,13 @@ ${finalVerifyResult.logs ?? ""}`;
346168
346581
  });
346169
346582
  if (healRes.ok) {
346170
346583
  const { healedCode } = await healRes.json();
346584
+ const failedTestPlan = authPlanFor(failedTest.tags, healedCode);
346171
346585
  const verifyResult = await runNativeWithLiveBrowser({
346172
346586
  playwrightCode: healedCode,
346173
346587
  baseUrl: run2.baseUrl,
346174
346588
  testVariables: run2.testVariables,
346589
+ storageStatePath: failedTestPlan.needsCookieState ? (0, import_runner_core12.getValidLocalAuthState)(runAuthCachePath) : void 0,
346590
+ authStandingMode: failedTestPlan.standingHeadersMode,
346175
346591
  validateContext: callbackContext
346176
346592
  });
346177
346593
  results.push({
@@ -347655,6 +348071,10 @@ ${result2.logs}`);
347655
348071
  log.warn(`\u26A0 Rate-limit block detected - posting ERROR (not FAILED) to skip heal/quarantine`);
347656
348072
  }
347657
348073
  }
348074
+ if (runStatus === "FAILED" && classifyErrorMessage(runError ?? null) === "config_issue") {
348075
+ runStatus = "ERROR";
348076
+ log.warn(`\u26A0 CONFIG_ISSUE from native execution - posting ERROR (not FAILED) to skip heal/quarantine`);
348077
+ }
347658
348078
  await postResult(opts.serverUrl, headers, run2.runId, {
347659
348079
  status: runStatus,
347660
348080
  stepResults: result.stepResults,
@@ -347670,7 +348090,7 @@ ${result2.logs}`);
347670
348090
  if (result.passed) {
347671
348091
  log.ok(`PASSED (${duration}ms)${result.suspiciousPass ? ` \u26A0 suspicious-pass (viewport ${Math.round(result.suspiciousPass.emptyRatio * 100)}% blank)` : ""}`);
347672
348092
  } else if (runStatus === "ERROR") {
347673
- log.warn(`BLOCKED (${duration}ms) - rate-limit detected, not counted as failure`);
348093
+ log.warn(`BLOCKED (${duration}ms) - environmental block (rate-limit / config-issue), not counted as failure`);
347674
348094
  } else {
347675
348095
  log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
347676
348096
  if (result.logs) {