@validate.qa/runner 1.0.43 → 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 +445 -54
  2. package/dist/cli.mjs +445 -54
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -4784,6 +4784,204 @@ var init_origin_domain = __esm({
4784
4784
  }
4785
4785
  });
4786
4786
 
4787
+ // ../shared/dist/auth-plan.js
4788
+ function hasInlineLoginSteps(playwrightCode) {
4789
+ if (!playwrightCode)
4790
+ return false;
4791
+ return /process\.env\.TEST_USER_EMAIL!?/.test(playwrightCode) && /process\.env\.TEST_USER_PASSWORD!?/.test(playwrightCode);
4792
+ }
4793
+ function hasGeneratedAuthGuard(playwrightCode) {
4794
+ if (!playwrightCode)
4795
+ return false;
4796
+ return /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
4797
+ }
4798
+ function legacyNeedsCookieState(tags, code, sharedAuthInjectEnabled2) {
4799
+ if (tags.includes("@auth"))
4800
+ return false;
4801
+ if (tags.includes("@api"))
4802
+ return true;
4803
+ if (!tags.includes("@needs-auth"))
4804
+ return false;
4805
+ if (sharedAuthInjectEnabled2 && tags.includes("@auth-guarded") && hasGeneratedAuthGuard(code))
4806
+ return true;
4807
+ const isGeneratedVariant = tags.includes("@edge") || tags.includes("@bug");
4808
+ const hasLegacyInlineLogin = tags.includes("@discovered") && !isGeneratedVariant && hasInlineLoginSteps(code);
4809
+ return !tags.includes("@inline-login") && !hasLegacyInlineLogin;
4810
+ }
4811
+ function explicitAuthTypeTags(t) {
4812
+ const found = /* @__PURE__ */ new Set();
4813
+ for (const tag of t) {
4814
+ if (AUTH_TYPE_TAGS.includes(tag))
4815
+ found.add(tag);
4816
+ else if (AUTH_TAG_ALIASES[tag])
4817
+ found.add(AUTH_TAG_ALIASES[tag]);
4818
+ }
4819
+ return [...found];
4820
+ }
4821
+ function invalidPlan(configurationError) {
4822
+ return {
4823
+ version: 1,
4824
+ source: "invalid",
4825
+ acquisition: "unsupported",
4826
+ presentation: "none",
4827
+ needsCookieState: false,
4828
+ needsStandingHeaders: false,
4829
+ standingHeadersMode: "none",
4830
+ needsLoginCreds: false,
4831
+ requiredEnvKeys: [],
4832
+ configurationError
4833
+ };
4834
+ }
4835
+ function explicitPlan(authTypeTag2, acquisition, presentation, opts) {
4836
+ return {
4837
+ version: 1,
4838
+ source: "explicit",
4839
+ authTypeTag: authTypeTag2,
4840
+ acquisition,
4841
+ presentation,
4842
+ needsCookieState: opts.cookie,
4843
+ needsStandingHeaders: opts.standingMode === "required",
4844
+ standingHeadersMode: opts.standingMode,
4845
+ needsLoginCreds: TEST_USER_ENV_KEYS.every((k) => opts.requiredEnvKeys.includes(k)),
4846
+ requiredEnvKeys: opts.requiredEnvKeys
4847
+ };
4848
+ }
4849
+ function deriveAuthPlan(tags, code, opts) {
4850
+ const t = Array.isArray(tags) ? tags : [];
4851
+ const sharedFlag = opts?.sharedAuthInjectEnabled ?? false;
4852
+ const isApi = t.includes("@api");
4853
+ const explicit = isApi ? explicitAuthTypeTags(t) : [];
4854
+ if (explicit.length > 0) {
4855
+ if (t.includes("@auth")) {
4856
+ return invalidPlan("@auth (self-login UI) cannot be combined with an explicit @api auth-type tag");
4857
+ }
4858
+ if (explicit.length > 1) {
4859
+ return invalidPlan(`conflicting auth-type tags: ${explicit.join(", ")}`);
4860
+ }
4861
+ switch (explicit[0]) {
4862
+ case AUTH_TAG_UNSUPPORTED:
4863
+ return {
4864
+ ...invalidPlan("auth scheme not yet drivable (e.g. HMAC / mTLS / provider-replay) \u2014 needs manual configuration")
4865
+ };
4866
+ case AUTH_TAG_NONE:
4867
+ return explicitPlan(AUTH_TAG_NONE, "none", "none", { cookie: false, standingMode: "none", requiredEnvKeys: [] });
4868
+ case AUTH_TAG_STANDING:
4869
+ return explicitPlan(AUTH_TAG_STANDING, "standing-secret", "standing-header", { cookie: false, standingMode: "required", requiredEnvKeys: [API_AUTH_HEADERS_ENV_KEY] });
4870
+ case AUTH_TAG_LOGIN_BEARER:
4871
+ return explicitPlan(AUTH_TAG_LOGIN_BEARER, "request-login", "bearer", { cookie: false, standingMode: "none", requiredEnvKeys: [...TEST_USER_ENV_KEYS] });
4872
+ case AUTH_TAG_LOGIN_COOKIE:
4873
+ return explicitPlan(AUTH_TAG_LOGIN_COOKIE, "request-login", "cookie", { cookie: false, standingMode: "legacy-if-configured", requiredEnvKeys: [...TEST_USER_ENV_KEYS] });
4874
+ case AUTH_TAG_COOKIE_STATE:
4875
+ return explicitPlan(AUTH_TAG_COOKIE_STATE, "browser-bootstrap", "cookie", { cookie: true, standingMode: "legacy-if-configured", requiredEnvKeys: [...TEST_USER_ENV_KEYS] });
4876
+ }
4877
+ }
4878
+ const cookie = legacyNeedsCookieState(t, code, sharedFlag);
4879
+ const authedLegacy = t.includes("@auth") || t.includes("@needs-auth") || isApi;
4880
+ return {
4881
+ version: 1,
4882
+ source: "legacy",
4883
+ acquisition: cookie ? "browser-bootstrap" : authedLegacy ? "request-login" : "none",
4884
+ presentation: authedLegacy ? "cookie" : "none",
4885
+ needsCookieState: cookie,
4886
+ needsStandingHeaders: false,
4887
+ // A legacy bare-@api test keeps injecting standing headers IF the project configured them
4888
+ // (preserving today's behavior) — but does not require them. Non-@api legacy tests never inject.
4889
+ standingHeadersMode: isApi ? "legacy-if-configured" : "none",
4890
+ needsLoginCreds: authedLegacy,
4891
+ requiredEnvKeys: authedLegacy ? [...TEST_USER_ENV_KEYS] : []
4892
+ };
4893
+ }
4894
+ function authTypeTag(tags) {
4895
+ const t = Array.isArray(tags) ? tags : [];
4896
+ const explicit = explicitAuthTypeTags(t);
4897
+ return explicit.length === 1 ? explicit[0] : void 0;
4898
+ }
4899
+ var AUTH_TAG_COOKIE_STATE, AUTH_TAG_LOGIN_COOKIE, AUTH_TAG_LOGIN_BEARER, AUTH_TAG_STANDING, AUTH_TAG_NONE, AUTH_TAG_UNSUPPORTED, AUTH_TYPE_TAGS, AUTH_TAG_ALIASES, TEST_USER_ENV_KEYS, API_AUTH_HEADERS_ENV_KEY;
4900
+ var init_auth_plan = __esm({
4901
+ "../shared/dist/auth-plan.js"() {
4902
+ "use strict";
4903
+ AUTH_TAG_COOKIE_STATE = "@auth-cookie-state";
4904
+ AUTH_TAG_LOGIN_COOKIE = "@auth-login-cookie";
4905
+ AUTH_TAG_LOGIN_BEARER = "@auth-login-bearer";
4906
+ AUTH_TAG_STANDING = "@auth-standing";
4907
+ AUTH_TAG_NONE = "@auth-none";
4908
+ AUTH_TAG_UNSUPPORTED = "@auth-unsupported";
4909
+ AUTH_TYPE_TAGS = [
4910
+ AUTH_TAG_COOKIE_STATE,
4911
+ AUTH_TAG_LOGIN_COOKIE,
4912
+ AUTH_TAG_LOGIN_BEARER,
4913
+ AUTH_TAG_STANDING,
4914
+ AUTH_TAG_NONE,
4915
+ AUTH_TAG_UNSUPPORTED
4916
+ ];
4917
+ AUTH_TAG_ALIASES = {
4918
+ "@auth-cookie": AUTH_TAG_COOKIE_STATE,
4919
+ "@auth-bearer": AUTH_TAG_LOGIN_BEARER,
4920
+ "@auth-token": AUTH_TAG_LOGIN_BEARER,
4921
+ "@auth-api-key": AUTH_TAG_STANDING,
4922
+ "@auth-standing-header": AUTH_TAG_STANDING
4923
+ };
4924
+ TEST_USER_ENV_KEYS = ["TEST_USER_EMAIL", "TEST_USER_PASSWORD"];
4925
+ API_AUTH_HEADERS_ENV_KEY = "API_AUTH_HEADERS";
4926
+ }
4927
+ });
4928
+
4929
+ // ../shared/dist/redact-needles.js
4930
+ function escapeRegExp(s) {
4931
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4932
+ }
4933
+ function buildNeedleMatcher(needles) {
4934
+ const usable = Array.from(new Set(needles.filter((n) => typeof n === "string" && n.length >= MIN_NEEDLE_LENGTH))).sort((a, b) => b.length - a.length);
4935
+ if (usable.length === 0)
4936
+ return null;
4937
+ return new RegExp(usable.map(escapeRegExp).join("|"), "g");
4938
+ }
4939
+ function redactNeedlesText(text, matcher) {
4940
+ if (!matcher || !text)
4941
+ return text;
4942
+ matcher.lastIndex = 0;
4943
+ const out = text.replace(matcher, REDACTED);
4944
+ return out === text ? text : out;
4945
+ }
4946
+ function redactNeedlesDeep(value, matcher) {
4947
+ if (!matcher)
4948
+ return value;
4949
+ if (typeof value === "string") {
4950
+ const scrubbed = redactNeedlesText(value, matcher);
4951
+ return scrubbed === value ? value : scrubbed;
4952
+ }
4953
+ if (Array.isArray(value)) {
4954
+ let changed = false;
4955
+ const next = value.map((item) => {
4956
+ const scrubbed = redactNeedlesDeep(item, matcher);
4957
+ if (scrubbed !== item)
4958
+ changed = true;
4959
+ return scrubbed;
4960
+ });
4961
+ return changed ? next : value;
4962
+ }
4963
+ if (value !== null && typeof value === "object") {
4964
+ let changed = false;
4965
+ const next = {};
4966
+ for (const [k, v] of Object.entries(value)) {
4967
+ const scrubbed = redactNeedlesDeep(v, matcher);
4968
+ if (scrubbed !== v)
4969
+ changed = true;
4970
+ next[k] = scrubbed;
4971
+ }
4972
+ return changed ? next : value;
4973
+ }
4974
+ return value;
4975
+ }
4976
+ var REDACTED, MIN_NEEDLE_LENGTH;
4977
+ var init_redact_needles = __esm({
4978
+ "../shared/dist/redact-needles.js"() {
4979
+ "use strict";
4980
+ REDACTED = "[REDACTED]";
4981
+ MIN_NEEDLE_LENGTH = 8;
4982
+ }
4983
+ });
4984
+
4787
4985
  // ../shared/dist/voice-config.js
4788
4986
  function normalizeTtsVoiceId(voiceId) {
4789
4987
  if (!voiceId)
@@ -5058,8 +5256,16 @@ __export(dist_exports, {
5058
5256
  ADDON_DEFINITIONS: () => ADDON_DEFINITIONS,
5059
5257
  ADMIN_EMAIL_LIMITS: () => ADMIN_EMAIL_LIMITS,
5060
5258
  AI_CREDIT_MARGIN: () => AI_CREDIT_MARGIN,
5259
+ API_AUTH_HEADERS_ENV_KEY: () => API_AUTH_HEADERS_ENV_KEY,
5061
5260
  API_PREFIX: () => API_PREFIX,
5062
5261
  AUDIO_CHUNK_DURATION_MS: () => AUDIO_CHUNK_DURATION_MS,
5262
+ AUTH_TAG_COOKIE_STATE: () => AUTH_TAG_COOKIE_STATE,
5263
+ AUTH_TAG_LOGIN_BEARER: () => AUTH_TAG_LOGIN_BEARER,
5264
+ AUTH_TAG_LOGIN_COOKIE: () => AUTH_TAG_LOGIN_COOKIE,
5265
+ AUTH_TAG_NONE: () => AUTH_TAG_NONE,
5266
+ AUTH_TAG_STANDING: () => AUTH_TAG_STANDING,
5267
+ AUTH_TAG_UNSUPPORTED: () => AUTH_TAG_UNSUPPORTED,
5268
+ AUTH_TYPE_TAGS: () => AUTH_TYPE_TAGS,
5063
5269
  COST_PER_M: () => COST_PER_M,
5064
5270
  CREDIT_PACKS_CENTS: () => CREDIT_PACKS_CENTS,
5065
5271
  CREDIT_PRICES_CENTS: () => CREDIT_PRICES_CENTS,
@@ -5102,6 +5308,7 @@ __export(dist_exports, {
5102
5308
  STT_MODEL_OPTIONS: () => STT_MODEL_OPTIONS,
5103
5309
  STT_PROVIDER_OPTIONS: () => STT_PROVIDER_OPTIONS,
5104
5310
  SUPPORT_EMAIL: () => SUPPORT_EMAIL,
5311
+ TEST_USER_ENV_KEYS: () => TEST_USER_ENV_KEYS,
5105
5312
  TRANSIENT_CONTROL_ROLES: () => TRANSIENT_CONTROL_ROLES,
5106
5313
  TRIAL_DURATION_DAYS: () => TRIAL_DURATION_DAYS,
5107
5314
  TTS_MODEL_OPTIONS: () => TTS_MODEL_OPTIONS,
@@ -5111,10 +5318,12 @@ __export(dist_exports, {
5111
5318
  accumulateStatements: () => accumulateStatements,
5112
5319
  aiCostToCreditCents: () => aiCostToCreditCents,
5113
5320
  analyzeMobileSelectorQuality: () => analyzeMobileSelectorQuality,
5321
+ authTypeTag: () => authTypeTag,
5114
5322
  bugFoundQuarantineWhere: () => bugFoundQuarantineWhere,
5115
5323
  buildAppiumCode: () => buildAppiumCode,
5116
5324
  buildLaunchStep: () => buildLaunchStep,
5117
5325
  buildMobileStep: () => buildMobileStep,
5326
+ buildNeedleMatcher: () => buildNeedleMatcher,
5118
5327
  buildSandboxedEnv: () => buildSandboxedEnv,
5119
5328
  buildSanitizedAgentEnv: () => buildSanitizedAgentEnv,
5120
5329
  buildStepTarget: () => buildStepTarget,
@@ -5130,6 +5339,7 @@ __export(dist_exports, {
5130
5339
  controlSignature: () => controlSignature,
5131
5340
  countInnerTests: () => countInnerTests,
5132
5341
  decideHarPosted: () => decideHarPosted,
5342
+ deriveAuthPlan: () => deriveAuthPlan,
5133
5343
  deriveProjectKey: () => deriveProjectKey,
5134
5344
  detectCrossOriginHtmlAsJson: () => detectCrossOriginHtmlAsJson,
5135
5345
  detectsErrorBannerAssertion: () => detectsErrorBannerAssertion,
@@ -5197,6 +5407,8 @@ __export(dist_exports, {
5197
5407
  planHasBillingPortal: () => planHasBillingPortal,
5198
5408
  planRequiresCheckout: () => planRequiresCheckout,
5199
5409
  platformTag: () => platformTag,
5410
+ redactNeedlesDeep: () => redactNeedlesDeep,
5411
+ redactNeedlesText: () => redactNeedlesText,
5200
5412
  registrableDomain: () => registrableDomain,
5201
5413
  resolveCrossOriginConfigOverride: () => resolveCrossOriginConfigOverride,
5202
5414
  resolveMobileStepValuePlaceholders: () => resolveMobileStepValuePlaceholders,
@@ -5239,6 +5451,8 @@ var init_dist = __esm({
5239
5451
  init_agent_env();
5240
5452
  init_test_counting();
5241
5453
  init_origin_domain();
5454
+ init_auth_plan();
5455
+ init_redact_needles();
5242
5456
  init_voice_config();
5243
5457
  init_feature_flags();
5244
5458
  }
@@ -52406,11 +52620,11 @@ var require_leac = __commonJS({
52406
52620
  if (r.str.length === 0) {
52407
52621
  throw new Error(`Rule #${i} ("${r.name}") has empty "str" property, which is not allowed.`);
52408
52622
  }
52409
- return new RegExp(escapeRegExp(r.str), "y");
52623
+ return new RegExp(escapeRegExp2(r.str), "y");
52410
52624
  }
52411
- return new RegExp(escapeRegExp(r.name), "y");
52625
+ return new RegExp(escapeRegExp2(r.name), "y");
52412
52626
  }
52413
- function escapeRegExp(str) {
52627
+ function escapeRegExp2(str) {
52414
52628
  return str.replace(/[-[\]{}()*+!<=:?./\\^$|#\s,]/g, "\\$&");
52415
52629
  }
52416
52630
  function toSticky(re) {
@@ -68383,7 +68597,7 @@ var require_discover_ai_planner = __commonJS({
68383
68597
  "../runner-core/dist/services/discover-ai-planner.js"(exports) {
68384
68598
  "use strict";
68385
68599
  Object.defineProperty(exports, "__esModule", { value: true });
68386
- exports.SCENARIO_SYSTEM_PROMPT = void 0;
68600
+ exports.PLANNER_STRENGTHEN_GATE_PROMPT = exports.SCENARIO_SYSTEM_PROMPT = void 0;
68387
68601
  exports.getAIPlannerMaxTokens = getAIPlannerMaxTokens;
68388
68602
  exports.getSnapshotConfidence = getSnapshotConfidence;
68389
68603
  exports.compressSiteMap = compressSiteMap;
@@ -68966,7 +69180,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
68966
69180
  4. **Multi-page journeys** - only when transitions were actually observed
68967
69181
  5. **Bug reports** - for observed 500 errors, crashes, or broken features
68968
69182
  6. **Steps are human-readable** - "Fill the email field", not "page.getByLabel('Email').fill()"
68969
- 7. **Expected outcomes are plain-English success criteria** - describe the user-visible product result, not the controls touched along the way. Steps already cover buttons, inputs, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test. **Outcome-strength self-check (applies to EVERY scenario type):** before emitting a scenario, ask \u2014 would every expectedOutcome still pass if the primary action did nothing and only the page and its existing chrome rendered? If yes, it is too weak. Add at least one evidence-backed post-condition that would FAIL in that no-op case: specific result content or value, a created or removed entity, a changed count or status, persisted state after reload, handled validation copy, or a concrete session signal. A route alone suffices only when navigation or redirect is itself the behavior under test; a newly revealed control suffices only when revealing that control is the behavior under test. Never invent copy to satisfy this check \u2014 Rule 1 still governs what you may assert.
69183
+ 7. **Expected outcomes are plain-English success criteria** - describe the user-visible product result, not the controls touched along the way. Steps already cover buttons, inputs, links, fields, headings, tabs, dialogs, and menus. Expected outcomes should say what the user can now do or rely on (record saved, message sent, search results filtered, validation explained, dashboard accessible). Include durable routes or exact copy only when they are the real product outcome; do NOT list "heading visible", "button visible", "search input visible", or similar element inventory unless that exact control is the behavior under test. **Outcome-strength check (per outcome, applies to EVERY scenario type):** evaluate each expectedOutcome separately. Delete it if it would already be true immediately before the scenario's primary action \u2014 UNLESS navigation, redirect, or revealing that state is itself the behavior under test. Keep at least one evidence-backed discriminator that proves the primary behavior actually occurred: specific result content or value, a created or removed entity, a changed count or status, persisted state after reload, or handled validation copy. Every value, count-change, persistence, creation, or removal assertion MUST trace to an earlier evidence-backed step that establishes the baseline or exact target. If the evidence does not support such a step, drop the assertion; do not invent setup. **Silent-failure BUG_REPORT exception:** apply ONLY when evidence explicitly records an action as broken because its intended state change did not occur. It does NOT apply to expected validation, permission denial, unmet prerequisites, loading or rate-limit states, or any non-BUG scenario. Prove the unchanged state with an evidence-backed before/after discriminator tied to the attempted run-scoped target \u2014 not merely "no toast", "no redirect", or "the form is still visible". Never invent copy to satisfy this check \u2014 Rule 1 still governs what you may assert.
68970
69184
  8. **Auth-aware** - mark scenarios that need login. Use process.env.TEST_USER_EMAIL / PASSWORD
68971
69185
  9. **No duplicates** - check existing test names before adding
68972
69186
  10. **All tests start from the landing page** - every generated test navigates to \`/\` first and uses the app's own navigation (clicking links/buttons) to reach the target page. Steps must reflect this: say "Navigate to landing page and click Sign In" not "Navigate to /auth/login". Deep-linking to internal routes causes hydration failures in SPAs. \`targetPages\` lists the pages involved in the flow.
@@ -68979,7 +69193,7 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
68979
69193
  Example - BAD happy path: \`["Navigate to /feature", "Click Create", "Verify dialog opens"]\` \u2192 only tests the trigger.
68980
69194
  Example - GOOD happy path (generic, async): \`["Navigate to landing and log in", "Navigate to the feature page", "Note the current count of items on the list", "Click the primary action button", "Fill required inputs and submit", "Wait for completion signal (up to 60s)", "Verify the item count increased by 1", "Open the newly created item", "Verify its content is non-empty and contains the input I provided"]\`
68981
69195
  Example - GOOD happy path (sync CRUD): \`["Navigate to landing and log in", "Navigate to the list page", "Click New", "Fill required fields", "Click Save", "Verify the new record appears in the list", "Click the record and verify its detail view shows the values I entered"]\`
68982
- 12. **Auth happy paths require concrete session proof.** A login/auth HAPPY_PATH is incomplete if it only proves "user reaches dashboard" or "authenticated page is visible." It MUST verify at least one durable signed-in signal that was observed in the evidence: user/account menu opens, Logout/Sign out is available, the observed display name/email/avatar/account item is present, authenticated navigation remains available after a reload, or another app-specific session control is usable. Prefer concrete observed labels from the evidence (for example "Logout", "Sign out", "Account", "Profile", or the user's own displayed name/email from evidence) over generic phrases like "personalized content." If the explorer only observed a redirect and no signed-in signal, keep the route outcome but mark the session-proof outcome reliability "low" and state what concrete signal was missing.
69196
+ 12. **Auth happy paths require concrete session proof.** A login/auth HAPPY_PATH is incomplete if it only proves "user reaches dashboard" or "authenticated page is visible." It MUST verify at least one durable signed-in signal that was observed in the evidence: user/account menu opens, Logout/Sign out is available, the observed display name/email/avatar/account item is present, authenticated navigation remains available after a reload, or another app-specific session control is usable. Prefer concrete observed labels from the evidence (for example "Logout", "Sign out", "Account", "Profile", or the user's own displayed name/email from evidence) over generic phrases like "personalized content." If the explorer only observed a redirect and no concrete signed-in control, do NOT invent one or instruct the executor to "open the account/user menu" \u2014 assert only the exact observed redirect (plus any directly supported transition, e.g. the login form is no longer shown), treat the redirect and any observed transition as transition outcomes \u2014 not concrete session proof \u2014 assign each its evidence-supported reliability, and name the missing concrete signal as a coverage gap in reasoning. Never substitute a menu of generic alternatives ("such as Logout, Profile, or \u2026") for one evidenced signal in a step or expected outcome.
68983
69197
  13. **Independent execution contract.** Every scenario MUST pass when run alone, first, last, in random order, and in parallel against an environment with no relevant feature data except explicitly guaranteed immutable fixtures. A scenario that depends on a sibling scenario's side effects (e.g. "delete the conversation that another test created") is invalid. If two flows are inherently coupled, bundle them as a single USER_JOURNEY scenario.
68984
69198
  14. **Scenario-owned state.** If a scenario reads, searches, opens, updates, messages, cancels, or deletes an entity, that scenario MUST create or seed the entity in its own setup steps. "Use an existing record" / "the conversation in the demo account" / "the existing user named X" are forbidden phrasings \u2014 they assume mutable pre-existing app data. Carve-out: baseline observations (current count, list length, "the dashboard heading is visible") are allowed as prerequisite reads ONLY when the durable assertion targets a self-created entity. Phrase setup as: "Click New, fill required fields with run-scoped values, submit and verify the new record appears, THEN proceed to <feature under test>."
68985
69199
  15. **Run-scoped test data.** Generated entities MUST use unique, run-scoped identifiers \u2014 phrase as "test message <Date.now()>" or "QA-<unique id>" in steps. Subsequent assertions MUST target that unique identifier, not "the first item", "the latest message", "the last row", or any visible display name observed during exploration. Hardcoding any specific display name, username, email, conversation title, or other identifier observed in the explorer's snapshot is forbidden \u2014 that data is incidental, not a stable contract. The same identifiers will not exist (or will look different) when the test runs in a fresh environment or against a different account.
@@ -69042,6 +69256,9 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
69042
69256
  Citing observed labels in \`expectedOutcomes\` is fine \u2014 that's asserting on stable product signals, not implementation specifics.
69043
69257
 
69044
69258
  ## Coverage Strategy
69259
+ **Coverage allocation \u2014 order matters.** The test budget is a ceiling, never a target. Before writing JSON, privately inventory each distinct behavior the evidence explicitly RECORDS: a SUCCESS, EMPTY_STATE, VALIDATION_ERROR, ERROR, BUG, an Explorer-Recorded Journey outcome, or an observed page/state transition. Assign each recorded behavior to exactly one scenario as its primary coverage owner \u2014 this does not prohibit repeating evidence-backed prerequisite setup and verification required by Rules 13\u201317. Bundle input variants that exercise the same purpose (e.g. two malformed-parameter cases that both return the same validation) into that one scenario. Use Page Intelligence only to make sure every described surface is reached \u2014 a merely listed control, route, provider, tab, or input does NOT by itself prove a behavior or earn its own scenario. Do NOT add an inferred scenario just to fill the budget. If a recorded behavior cannot be tested independently without unsupported mutable state, name it as a coverage gap (rule 17) rather than pretending it is self-contained. Treat a page/state transition as a distinct behavior only when navigation or revealing that state is itself the recorded product outcome; otherwise keep it as a step within the owning scenario \u2014 Page Intelligence creates no additional coverage obligation.
69260
+
69261
+ Apply the per-surface minimums below only where the evidence actually supports them (evidence support includes Rule 1's trigger-only structural exception):
69045
69262
  - For every form: at least 1 HAPPY_PATH (submit valid data AND verify durable result per rule 11) + 1 EDGE_CASE (empty/invalid submission)
69046
69263
  - For auth flows: login happy path (verify the post-login route AND a concrete signed-in/session signal per rule 12) + validation (wrong password, empty fields)
69047
69264
  - For CRUD: each scenario MUST own the entity it operates on. Read/search/update/delete/cancel-on-existing/view-existing scenarios MUST include a create step earlier in the same scenario (or declare an immutable seed fixture by name). Never assume a record from prior runs or sibling tests exists. Splitting into atomic create/read/update/delete tests is fine ONLY when each test creates its own entity. When create/delete are tightly coupled (e.g. delete-only with no create flow elsewhere), bundle into a single USER_JOURNEY: create \u2192 verify \u2192 delete \u2192 verify removal.
@@ -69071,11 +69288,11 @@ Return ONLY one valid JSON object matching the schema below. Do NOT include <thi
69071
69288
  { "action": "Fill the email field with test credentials" },
69072
69289
  { "action": "Fill the password field with test credentials" },
69073
69290
  { "action": "Click the Sign In button" },
69074
- { "action": "Open the observed authenticated account or user menu" }
69291
+ { "action": "After reaching the observed post-login page, reload it" }
69075
69292
  ],
69076
69293
  "expectedOutcomes": [
69077
- { "type": "url", "description": "User reaches /dashboard after signing in", "reliability": "high" },
69078
- { "type": "element-visible", "description": "The signed-in session is proven by an observed user/account signal such as Logout, Profile, or the user's displayed identity", "reliability": "high" }
69294
+ { "type": "url", "description": "User reaches /dashboard after signing in and remains there after reload", "reliability": "high" },
69295
+ { "type": "element-hidden", "description": "The login form does not return after reload", "reliability": "medium" }
69079
69296
  ],
69080
69297
  "targetPages": ["/auth/login", "/dashboard"],
69081
69298
  "prerequisites": []
@@ -69105,6 +69322,46 @@ Each entry is exactly one of:
69105
69322
  3 = medium (navigation, secondary features)
69106
69323
  4 = low (edge cases, cosmetic)
69107
69324
  5 = nice-to-have`;
69325
+ exports.PLANNER_STRENGTHEN_GATE_PROMPT = `You are a senior QA lead performing ONE design-audit pass over a test PLAN another model wrote for a single web feature. The plan will be handed to a LIVE generator that opens the real app, follows each scenario's steps, observes the DOM, and writes the concrete assertions itself. Division of labor you MUST respect:
69326
+ - The PLAN owns the ORACLE: WHAT must be true for the behavior to be correct, stated in plain English.
69327
+ - The GENERATOR owns BINDING: locators and the exact runtime values it reads off the live page.
69328
+ Never write an expectation whose truth is decided by looking at the current app ("whatever the page shows"): that lets a live bug become the expected result. State the intended truth; the generator will bind it and report a mismatch if reality disagrees.
69329
+
69330
+ You are given (1) the EXACT evidence the plan was based on \u2014 the ONLY ground truth; you may use it to know WHAT behaviors exist, but you must NEVER introduce a behavior, route, element, or effect not present in or directly implied by it \u2014 and (2) the PLAN as JSON. Return the complete corrected plan.
69331
+
69332
+ ## Rule 0 \u2014 REPAIR OR DROP, never merely describe (highest priority)
69333
+ Do NOT write a reasoning note about a fix you did not actually apply. Every change you claim must be present in the scenarios you output: if you say you strengthened an outcome, the new outcome text must be there; if you say you merged or removed a duplicate, NO surviving scenario may share the same primary action + outcome. If a defect cannot be repaired from the evidence, DROP the scenario or FLAG it \u2014 do not keep a known-weak scenario with an excuse. Flag-and-keep is forbidden.
69334
+
69335
+ Your only legal moves per scenario are: KEEP unchanged \xB7 REWRITE using a grounded fact \xB7 DROP as unsupported/duplicate \xB7 FLAG as needs-more-evidence (low reliability, honest note). You may never FORCE-REWRITE by inventing an effect the evidence does not show (a visible Delete control does not tell you whether delete removes, archives, hides, or disables \u2014 do not assume).
69336
+
69337
+ ## Rule 1 \u2014 Classify each scenario, then require the matching oracle
69338
+ Identify the scenario's archetype and hold its expectedOutcomes to the right shape. An outcome is only valid if it would be FALSE under a plausible FAULT for that archetype:
69339
+
69340
+ - CREATE/EDIT/DELETE (mutation): the scenario must control its own target \u2014 create the entity it acts on with a self-chosen unique identifier (create \u2192 act \u2192 verify), or declare a prerequisite that resolves it (a fixture or a stable immutable reference). Never depend on unspecified pre-existing/ambient rows ("delete the first row", "open an existing order"). Oracle = the owned entity's before\u2192after: it appears after create / reflects the edit / is absent after delete (verify by the unique identifier, ideally after reload), while an unrelated sentinel is unaffected. Plausible faults to defeat: no-op, wrong-target, duplicate, non-persistence-after-reload, over-broad delete.
69341
+ - LIST/READ/DETAIL: oracle = the returned content corresponds to what was requested (the opened item's identifier matches the one selected). No before\u2192after is required.
69342
+ - FILTER/SEARCH/SORT: oracle = a delta \u2014 a known in-scope item remains AND a known out-of-scope item disappears (or order changes as specified). "Matching items are shown" alone is a no-op (a broken filter that shows everything passes).
69343
+ - VALIDATION/NEGATIVE: oracle must distinguish "the action was correctly rejected by validation" from "nothing happened / a dead control". Prefer the grounded error signal the evidence recorded (exact message or the specific field marked invalid) plus the absence of the forbidden effect (no entity created). If no error signal was observed, keep it structural at low reliability and FLAG.
69344
+ - NAVIGATION/REDIRECT/AUTH-GUARD: the destination is a legitimate primary effect \u2014 assert the route AND one observed landing signal (heading/content) so a redirect to a broken page fails. Route-presence as an outcome on a NON-navigation scenario is padding \u2014 drop it.
69345
+ - TOGGLE/IDEMPOTENT: assert the changed attribute/state (e.g. pressed/checked/disabled), or for idempotent actions that no duplicate is created \u2014 not mere visibility.
69346
+ - EXTERNAL INTEGRATION: assert the outbound URL/payload contains the expected parameters (drawn from evidence), not a frozen full string.
69347
+ - STATIC CONTENT: presence/label oracle is acceptable; do not inflate it. Verify shared chrome (nav/footer) ONCE, not per page.
69348
+ - APP BUG: exactly one BUG_REPORT per distinct root cause; assert the observed defective effect. Console errors, thrown exceptions, and network status codes are NOT page text \u2014 assert the observable page effect (no toast / no redirect / form unchanged), or set the outcome type to the right probe.
69349
+
69350
+ ## Rule 2 \u2014 Exact values: judge by ORIGIN, not specificity
69351
+ - DROP as brittle: mutable ambient snapshots the test does not control \u2014 current counts ("8 cards"), seeded record names, other users' data, opaque IDs/slugs, phone numbers. Phrase these relationally ("the account's active business name", "the created item", "\u22651 result").
69352
+ - KEEP exact: values the SCENARIO itself introduced (a unique title it typed \u2014 assert it propagates), app-authored static strings (validation messages, headings, template copy, fixed error codes), and stable contract/derived facts (a contractual price, total = qty \xD7 unit price). These are real oracles, not brittleness.
69353
+
69354
+ ## Rule 3 \u2014 Do not build on flows the evidence shows are broken or unrepeatable
69355
+ If evidence flags a route/flow as broken, a precondition as unobtainable (e.g. a single-use token already consumed, an account locked out), or a mutation as irreversible/one-shot on a shared account, you MUST NOT emit happy-path scenarios that depend on that flow succeeding or repeating. Reconcile: drop the dependent happy path, or route it through an alternate path the evidence actually shows.
69356
+
69357
+ ## Rule 4 \u2014 Suite economy and completeness
69358
+ Budget is a ceiling, not a target. Merge scenarios that collide on (route + primary action + outcome-class). Do not enumerate one scenario per item across a long list when 1\u20133 representatives suffice. Reclaim freed budget to cover a DISTINCT behavior the evidence recorded that the plan left untested (swap, do not inflate). An action inferred only from a merely-listed control the evidence never showed exercised should be dropped and FLAGGED as a survey gap, not asserted.
69359
+
69360
+ ## Rule 5 \u2014 reliability is honest
69361
+ high = the effect and its trigger are both directly observed in evidence; medium = observed but partial/indirect; low = structural or inferred (must be FLAGGED). Never launder an unsupported claim by labelling it low \u2014 if it is invented, drop it.
69362
+
69363
+ ## Output
69364
+ Keep the exact JSON schema { "featureGroups":[...], "scenarios":[...], "reasoning":"..." }; each scenario keeps name, type, priority, variant, featureGroup, goal, steps[], expectedOutcomes[{type,description,reliability}], targetPages, prerequisites. In "reasoning", state ONLY changes you actually made (kept/rewritten/dropped/merged/flagged counts). Return ONLY the complete corrected JSON object \u2014 no markdown, no prose outside JSON.`;
69108
69365
  function coerceRawPrerequisites(raw) {
69109
69366
  if (!Array.isArray(raw))
69110
69367
  return [];
@@ -69888,6 +70145,95 @@ Each entry is exactly one of:
69888
70145
  }
69889
70146
  }
69890
70147
  }
70148
+ const gateFlag = (process.env.PLANNER_STRENGTHEN_GATE || "").trim().toLowerCase();
70149
+ const gateEnabled = gateFlag !== "false" && gateFlag !== "0";
70150
+ if (gateEnabled && context.mode !== "survey" && !options.systemPromptOverride && !options.userPromptOverride && !extraPlannerCallUsed && scenarios.length > 0) {
70151
+ const gateModel = options.gateModelOverride ? (0, ai_provider_js_1.resolveModelForTask)(options.gateModelOverride, "agent") : agentModel;
70152
+ const gateClient = options.gateModelOverride ? (0, ai_provider_js_1.getRunnerClientForModel)(options.gateModelOverride) : aiClient;
70153
+ const gateMaxTokens = getAIPlannerMaxTokens(gateModel);
70154
+ const gatePlan = { featureGroups, scenarios, reasoning: aiOutput.reasoning ?? "" };
70155
+ const gateUser = `EVIDENCE (ground truth):
70156
+ ${userPrompt}
70157
+
70158
+ =====
70159
+
70160
+ PLAN TO AUDIT AND REPAIR (JSON):
70161
+ ${JSON.stringify(gatePlan)}`;
70162
+ const gateStart = Date.now();
70163
+ let gateResponse = null;
70164
+ try {
70165
+ extraPlannerCallUsed = true;
70166
+ gateResponse = await (0, ai_retry_js_1.resilientAICall)((signal, arm) => (0, ai_queue_js_1.queuedAICall)(() => {
70167
+ arm();
70168
+ return gateClient.chat.completions.create({
70169
+ model: gateModel,
70170
+ messages: [
70171
+ { role: "system", content: exports.PLANNER_STRENGTHEN_GATE_PROMPT },
70172
+ { role: "user", content: gateUser }
70173
+ ],
70174
+ max_tokens: gateMaxTokens,
70175
+ temperature: 0.2,
70176
+ ...(0, ai_provider_js_1.getJsonObjectResponseFormat)(gateModel),
70177
+ ...(0, ai_provider_js_1.getNoThinkingRequestOptions)(gateModel)
70178
+ }, { signal });
70179
+ }), {
70180
+ label: "ai-planner-strengthen-gate",
70181
+ deferTimeoutUntilArmed: true,
70182
+ onRetry: (attempt, reason, delay) => logs2.push(`AI planner strengthen-gate call failed (${reason}) \u2014 retry ${attempt} in ${Math.round(delay / 1e3)}s`)
70183
+ });
70184
+ } catch (err) {
70185
+ const message = err instanceof Error ? err.message : String(err);
70186
+ logs2.push(`AI planner strengthen-gate failed (${message}) \u2014 keeping pre-gate result`);
70187
+ }
70188
+ if (gateResponse) {
70189
+ try {
70190
+ options.onAICall?.({
70191
+ iteration: 1,
70192
+ groupId: `ai-planner-${context.projectId.slice(0, 8)}`,
70193
+ callType: "discoveryAIPlannerStrengthenGate",
70194
+ model: gateModel,
70195
+ provider: (0, ai_provider_js_1.getProviderForModel)(gateModel),
70196
+ durationMs: Date.now() - gateStart,
70197
+ promptTokens: gateResponse.usage?.prompt_tokens,
70198
+ completionTokens: gateResponse.usage?.completion_tokens,
70199
+ totalTokens: gateResponse.usage?.total_tokens,
70200
+ assistantMessage: gateResponse.choices[0]?.message?.content,
70201
+ requestMessageCount: 2
70202
+ });
70203
+ } catch {
70204
+ }
70205
+ const gateRaw = gateResponse.choices[0]?.message?.content?.trim();
70206
+ if (gateRaw) {
70207
+ let gateOut = null;
70208
+ try {
70209
+ gateOut = JSON.parse((0, strip_json_fences_js_1.stripJsonFences)(gateRaw));
70210
+ } catch (err) {
70211
+ const message = err instanceof Error ? err.message : String(err);
70212
+ logs2.push(`AI planner strengthen-gate returned invalid JSON (${message}) \u2014 keeping pre-gate result`);
70213
+ }
70214
+ if (gateOut) {
70215
+ try {
70216
+ const vg = validateAIPlannerOutput(gateOut, siteMap, context.existingTestNames, context.testBudget, evidence.recordedJourneys, context.mode, context.focusAreas);
70217
+ const preGateCount = scenarios.length;
70218
+ const keepsCoverage = vg.scenarios.length >= Math.ceil(preGateCount * 0.5);
70219
+ const noWorseGaps = vg.coverageGaps.length <= coverageGaps.length;
70220
+ if (vg.scenarios.length > 0 && keepsCoverage && noWorseGaps) {
70221
+ logs2.push(`AI planner strengthen-gate [${gateModel}]: ${preGateCount}\u2192${vg.scenarios.length} scenarios, gaps ${coverageGaps.length}\u2192${vg.coverageGaps.length}; taking gated result`);
70222
+ scenarios = vg.scenarios;
70223
+ featureGroups = vg.featureGroups;
70224
+ warnings = [...warnings, ...vg.warnings];
70225
+ coverageGaps = vg.coverageGaps;
70226
+ } else {
70227
+ logs2.push(`AI planner strengthen-gate rejected (scen ${preGateCount}\u2192${vg.scenarios.length}, gaps ${coverageGaps.length}\u2192${vg.coverageGaps.length}); keeping pre-gate result`);
70228
+ }
70229
+ } catch (err) {
70230
+ const message = err instanceof Error ? err.message : String(err);
70231
+ logs2.push(`AI planner strengthen-gate validation threw (${message}) \u2014 keeping pre-gate result`);
70232
+ }
70233
+ }
70234
+ }
70235
+ }
70236
+ }
69891
70237
  const totalFinal = scenarios.length + coverageGaps.length;
69892
70238
  const rejectionRateFinal = totalFinal > 0 ? coverageGaps.length / totalFinal : 0;
69893
70239
  if (rejectionRateFinal > 0.5 && totalFinal >= 3) {
@@ -73667,7 +74013,7 @@ var require_mobile_heal_postprocess = __commonJS({
73667
74013
  // OpenAI-style secret key
73668
74014
  ];
73669
74015
  var EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
73670
- function escapeRegExp(value) {
74016
+ function escapeRegExp2(value) {
73671
74017
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
73672
74018
  }
73673
74019
  function collectSecretVars(testVariables, minLen, keyRe) {
@@ -73686,7 +74032,7 @@ var require_mobile_heal_postprocess = __commonJS({
73686
74032
  return text;
73687
74033
  let out = text;
73688
74034
  for (const { key, value } of collectSecretVars(testVariables, MIN_REDACT_LEN, REDACT_KEY_RE)) {
73689
- out = out.replace(new RegExp(escapeRegExp(value), "g"), `{{${key}}}`);
74035
+ out = out.replace(new RegExp(escapeRegExp2(value), "g"), `{{${key}}}`);
73690
74036
  }
73691
74037
  for (const source of TOKEN_SOURCES) {
73692
74038
  out = out.replace(new RegExp(source, "g"), "[REDACTED]");
@@ -76086,7 +76432,7 @@ var require_api_auth_headers = __commonJS({
76086
76432
  "../runner-core/dist/services/api-auth-headers.js"(exports) {
76087
76433
  "use strict";
76088
76434
  Object.defineProperty(exports, "__esModule", { value: true });
76089
- exports.parseApiAuthHeaders = parseApiAuthHeaders;
76435
+ exports.parseApiAuthHeaders = parseApiAuthHeaders2;
76090
76436
  exports.resolveApiAuthHeaders = resolveApiAuthHeaders;
76091
76437
  exports.isAppOriginRequest = isAppOriginRequest;
76092
76438
  exports.buildOriginScopedApiFixtures = buildOriginScopedApiFixtures;
@@ -76099,7 +76445,7 @@ var require_api_auth_headers = __commonJS({
76099
76445
  }
76100
76446
  return false;
76101
76447
  }
76102
- function parseApiAuthHeaders(raw) {
76448
+ function parseApiAuthHeaders2(raw) {
76103
76449
  if (!raw || typeof raw !== "string")
76104
76450
  return void 0;
76105
76451
  const trimmed = raw.trim();
@@ -76123,9 +76469,9 @@ var require_api_auth_headers = __commonJS({
76123
76469
  function resolveApiAuthHeaders(input) {
76124
76470
  if (input.explicit)
76125
76471
  return input.explicit;
76126
- if (!input.isApiTest)
76472
+ if (input.standingHeadersMode === "none")
76127
76473
  return void 0;
76128
- return parseApiAuthHeaders(input.apiAuthHeadersVar);
76474
+ return parseApiAuthHeaders2(input.apiAuthHeadersVar);
76129
76475
  }
76130
76476
  function isAppOriginRequest(url, baseUrl) {
76131
76477
  if (typeof url !== "string" || !baseUrl)
@@ -76392,7 +76738,7 @@ var require_playwright_native = __commonJS({
76392
76738
  var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
76393
76739
  var PW_CLI = __require.resolve("@playwright/test/cli");
76394
76740
  async function executeTestViaNativePlaywright2(options) {
76395
- const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
76741
+ const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, authStandingMode, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
76396
76742
  const logs2 = [];
76397
76743
  const screenshots = [];
76398
76744
  logs2.push(`\u{1F3AD} Native Playwright starting`);
@@ -76402,16 +76748,19 @@ var require_playwright_native = __commonJS({
76402
76748
  logs2.push("\u26A0\uFE0F WARNING: baseUrl is empty - test will likely fail with navigation errors");
76403
76749
  }
76404
76750
  let runDir = null;
76751
+ let standingNeedleMatcher = null;
76405
76752
  let callbackProxy;
76406
76753
  try {
76407
76754
  runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
76408
76755
  await (0, promises_1.mkdir)(runDir, { recursive: true });
76409
76756
  const isApiTest2 = playwrightCode.includes("@api") || playwrightCode.includes("{ request }") && !playwrightCode.includes("{ page");
76757
+ const effectiveStandingMode = isApiTest2 ? authStandingMode ?? "legacy-if-configured" : "none";
76410
76758
  const resolvedExtraHeaders = (0, api_auth_headers_js_1.resolveApiAuthHeaders)({
76411
76759
  explicit: extraHttpHeaders,
76412
- isApiTest: isApiTest2,
76760
+ standingHeadersMode: effectiveStandingMode,
76413
76761
  apiAuthHeadersVar: testVariables?.API_AUTH_HEADERS
76414
76762
  });
76763
+ standingNeedleMatcher = (0, shared_1.buildNeedleMatcher)(resolvedExtraHeaders ? Object.values(resolvedExtraHeaders) : []);
76415
76764
  const cssDisableSetup = `
76416
76765
  const __qaLiveBrowserStateFile = process.env.VALIDATE_LIVE_BROWSER_STATE_PATH;
76417
76766
  let __qaLiveBrowserInterval: ReturnType<typeof setInterval> | null = null;
@@ -76606,6 +76955,18 @@ test.afterEach(async ({ page }, testInfo) => {
76606
76955
  logs2.push("\u26A0 Could not find @playwright/test import - animation disabling NOT injected");
76607
76956
  }
76608
76957
  const useScopedApiHeaders = isApiTest2 && !!resolvedExtraHeaders && !!importMatch && !!baseUrl;
76958
+ const authDelivery = { standingHeadersConfigured: !!resolvedExtraHeaders, standingHeadersInjected: useScopedApiHeaders };
76959
+ if (resolvedExtraHeaders && !useScopedApiHeaders) {
76960
+ logs2.push("\u26A0 Standing API auth headers were configured but could not be installed origin-scoped \u2014 failing closed (no global fallback).");
76961
+ return {
76962
+ passed: false,
76963
+ stepResults: [],
76964
+ screenshots: [],
76965
+ logs: logs2.join("\n"),
76966
+ error: 'CONFIG_ISSUE: Standing API auth headers were configured but could not be installed origin-scoped (needs a rewritable `import { test } from "@playwright/test"` and BASE_URL); refusing to attach them globally to avoid an off-origin credential leak.',
76967
+ authDelivery
76968
+ };
76969
+ }
76609
76970
  let instrumentedCode;
76610
76971
  if (isApiTest2) {
76611
76972
  instrumentedCode = useScopedApiHeaders ? playwrightCode.replace(importMatch[0], importMatch[0].replace(/from\s+['"]@playwright\/test['"]/, "from './qa-api-fixtures'")) : playwrightCode;
@@ -76629,7 +76990,7 @@ test.afterEach(async ({ page }, testInfo) => {
76629
76990
  const absHarPath = wantHar ? (0, path_1.join)(runDir, "session.har").replace(/\\/g, "/") : null;
76630
76991
  const recordHarLine = absHarPath ? `recordHar: { path: ${JSON.stringify(absHarPath)}, mode: 'full' },` : "";
76631
76992
  const storageStateLine = storageStatePath ? `storageState: ${JSON.stringify(storageStatePath.replace(/\\/g, "/"))},` : "";
76632
- const extraHeadersLine = resolvedExtraHeaders && !useScopedApiHeaders && Object.keys(resolvedExtraHeaders).length > 0 ? `extraHTTPHeaders: ${JSON.stringify(resolvedExtraHeaders)},` : "";
76993
+ const extraHeadersLine = "";
76633
76994
  const launchArgs = IS_CLOUD2 ? "args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', '--no-zygote', '--disable-accelerated-2d-canvas', '--js-flags=--max-old-space-size=512']," : "args: ['--disable-blink-features=AutomationControlled'],";
76634
76995
  const config = `
76635
76996
  // Plain object export - no require('@playwright/test') needed.
@@ -76767,7 +77128,7 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76767
77128
  logs2.push(`\u26A0 HAR parse skipped: ${msg.slice(0, 200)}`);
76768
77129
  }
76769
77130
  }
76770
- return {
77131
+ const nativeResult = {
76771
77132
  passed,
76772
77133
  stepResults,
76773
77134
  screenshots,
@@ -76776,19 +77137,24 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76776
77137
  harApiCalls,
76777
77138
  logs: logs2.join("\n"),
76778
77139
  error: passed ? void 0 : error2,
76779
- suspiciousPass
77140
+ suspiciousPass,
77141
+ authDelivery
76780
77142
  };
77143
+ if (!standingNeedleMatcher)
77144
+ return nativeResult;
77145
+ return (0, shared_1.redactNeedlesDeep)(nativeResult, standingNeedleMatcher);
76781
77146
  } catch (fatalErr) {
76782
77147
  const msg = fatalErr instanceof Error ? fatalErr.message : String(fatalErr);
76783
77148
  logs2.push(`
76784
77149
  \u{1F4A5} Fatal error: ${msg}`);
76785
- return {
77150
+ const fatalResult = {
76786
77151
  passed: false,
76787
77152
  stepResults: [],
76788
77153
  screenshots,
76789
77154
  logs: logs2.join("\n"),
76790
77155
  error: msg
76791
77156
  };
77157
+ return standingNeedleMatcher ? (0, shared_1.redactNeedlesDeep)(fatalResult, standingNeedleMatcher) : fatalResult;
76792
77158
  } finally {
76793
77159
  await callbackProxy?.close().catch(() => {
76794
77160
  });
@@ -85892,7 +86258,7 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
85892
86258
  }
85893
86259
  return result;
85894
86260
  }
85895
- function escapeRegExp(value) {
86261
+ function escapeRegExp2(value) {
85896
86262
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
85897
86263
  }
85898
86264
  function buildSensitiveEnvReplacements(testVariables) {
@@ -85914,7 +86280,7 @@ ${elementContext}${formContext}${observationContext}${interactionContext}${lowCo
85914
86280
  for (const replacement of buildSensitiveEnvReplacements(testVariables)) {
85915
86281
  if (replacement.value.includes("\n") || replacement.value.includes("\r"))
85916
86282
  continue;
85917
- const literalPattern = new RegExp(`(['"\`])${escapeRegExp(replacement.value)}\\1`, "g");
86283
+ const literalPattern = new RegExp(`(['"\`])${escapeRegExp2(replacement.value)}\\1`, "g");
85918
86284
  result = result.replace(literalPattern, replacement.envRef);
85919
86285
  }
85920
86286
  return result;
@@ -255170,7 +255536,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
255170
255536
  }
255171
255537
  }
255172
255538
  return result;
255173
- function escapeRegExp(str) {
255539
+ function escapeRegExp2(str) {
255174
255540
  return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
255175
255541
  }
255176
255542
  function getTodoCommentsRegExp() {
@@ -255178,7 +255544,7 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")}
255178
255544
  const multiLineCommentStart = /(?:\/\*+\s*)/.source;
255179
255545
  const anyNumberOfSpacesAndAsterisksAtStartOfLine = /(?:^(?:\s|\*)*)/.source;
255180
255546
  const preamble = "(" + anyNumberOfSpacesAndAsterisksAtStartOfLine + "|" + singleLineCommentStart + "|" + multiLineCommentStart + ")";
255181
- const literals = "(?:" + map2(descriptors, (d) => "(" + escapeRegExp(d.text) + ")").join("|") + ")";
255547
+ const literals = "(?:" + map2(descriptors, (d) => "(" + escapeRegExp2(d.text) + ")").join("|") + ")";
255182
255548
  const endOfLineOrEndOfComment = /(?:$|\*\/)/.source;
255183
255549
  const messageRemainder = /(?:.*?)/.source;
255184
255550
  const messagePortion = "(" + literals + messageRemainder + ")";
@@ -303438,7 +303804,7 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
303438
303804
  confidence: "low"
303439
303805
  };
303440
303806
  }
303441
- function escapeRegExp(s) {
303807
+ function escapeRegExp2(s) {
303442
303808
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
303443
303809
  }
303444
303810
  function tryDeterministicStrictModeFix(code, error2) {
@@ -303461,10 +303827,10 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
303461
303827
  const [, , role, , name, restOfOptions] = violated;
303462
303828
  if (/exact\s*:\s*true/.test(restOfOptions))
303463
303829
  return null;
303464
- const akaPattern = new RegExp(`aka\\s+getByRole\\(\\s*(['"\`])${escapeRegExp(role)}\\1\\s*,\\s*\\{[^}]*?name:\\s*(['"\`])${escapeRegExp(name)}\\2[^}]*?exact:\\s*true[^}]*\\}\\s*\\)`);
303830
+ const akaPattern = new RegExp(`aka\\s+getByRole\\(\\s*(['"\`])${escapeRegExp2(role)}\\1\\s*,\\s*\\{[^}]*?name:\\s*(['"\`])${escapeRegExp2(name)}\\2[^}]*?exact:\\s*true[^}]*\\}\\s*\\)`);
303465
303831
  if (!akaPattern.test(error2))
303466
303832
  return null;
303467
- const codePattern = new RegExp(`(getByRole\\(\\s*(['"\`])${escapeRegExp(role)}\\2\\s*,\\s*\\{[^}]*?name:\\s*(['"\`])${escapeRegExp(name)}\\3)([^}]*\\})`, "g");
303833
+ const codePattern = new RegExp(`(getByRole\\(\\s*(['"\`])${escapeRegExp2(role)}\\2\\s*,\\s*\\{[^}]*?name:\\s*(['"\`])${escapeRegExp2(name)}\\3)([^}]*\\})`, "g");
303468
303834
  let modified = false;
303469
303835
  const patched = code.replace(codePattern, (full, prefix, _q1, _q2, suffix) => {
303470
303836
  if (/exact\s*:\s*true/.test(suffix))
@@ -303481,11 +303847,11 @@ ${(verifyResult.logs ?? "").slice(0, 2e3)}`;
303481
303847
  const [, , text, optionsBlock] = violated;
303482
303848
  if (optionsBlock && /exact\s*:\s*true/.test(optionsBlock))
303483
303849
  return null;
303484
- const akaPattern = new RegExp(`aka\\s+getByText\\(\\s*(['"\`])${escapeRegExp(text)}\\1\\s*,\\s*\\{[^}]*?exact:\\s*true[^}]*\\}\\s*\\)`);
303850
+ const akaPattern = new RegExp(`aka\\s+getByText\\(\\s*(['"\`])${escapeRegExp2(text)}\\1\\s*,\\s*\\{[^}]*?exact:\\s*true[^}]*\\}\\s*\\)`);
303485
303851
  if (!akaPattern.test(error2))
303486
303852
  return null;
303487
- const noOptsPattern = new RegExp(`getByText\\(\\s*(['"\`])${escapeRegExp(text)}\\1\\s*\\)`, "g");
303488
- const withOptsPattern = new RegExp(`(getByText\\(\\s*(['"\`])${escapeRegExp(text)}\\2\\s*,\\s*\\{)([^}]*\\})`, "g");
303853
+ const noOptsPattern = new RegExp(`getByText\\(\\s*(['"\`])${escapeRegExp2(text)}\\1\\s*\\)`, "g");
303854
+ const withOptsPattern = new RegExp(`(getByText\\(\\s*(['"\`])${escapeRegExp2(text)}\\2\\s*,\\s*\\{)([^}]*\\})`, "g");
303489
303855
  let modified = false;
303490
303856
  let patched = code.replace(noOptsPattern, (_full, q) => {
303491
303857
  modified = true;
@@ -321442,9 +321808,9 @@ var require_tool_ledger = __commonJS({
321442
321808
  const ledgerPath = (0, node_path_1.join)(runDir, "discovery-events.jsonl");
321443
321809
  let checkpointIndex = 0;
321444
321810
  let headerWritten = false;
321445
- const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
321811
+ const escapeRegExp2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
321446
321812
  const credentialScrubbers = Array.from(credentialEnvMap.entries()).filter(([, value]) => typeof value === "string" && value.length > 0).sort((a, b) => b[1].length - a[1].length).map(([envName, value]) => ({
321447
- re: new RegExp(escapeRegExp(value), "g"),
321813
+ re: new RegExp(escapeRegExp2(value), "g"),
321448
321814
  replacement: `[REDACTED_${envName}]`
321449
321815
  }));
321450
321816
  const scrubCredentials = (text) => {
@@ -323604,6 +323970,7 @@ Explore complete: ${exploreResult.pagesDiscovered} pages, ${exploreResult.elemen
323604
323970
  testBudget
323605
323971
  }, {
323606
323972
  modelOverride: ctx.planModel ?? ctx.generateModel,
323973
+ gateModelOverride: ctx.planGateModel,
323607
323974
  onAICall
323608
323975
  });
323609
323976
  logs2.push(...planResult.logs);
@@ -324182,6 +324549,7 @@ Plan (reused): ${resumeSavedScenarios.length} scenarios from the paused run.`);
324182
324549
  testBudget: featureTestBudget
324183
324550
  }, {
324184
324551
  modelOverride: ctx.planModel ?? ctx.generateModel,
324552
+ gateModelOverride: ctx.planGateModel,
324185
324553
  onAICall
324186
324554
  });
324187
324555
  logs2.push(...planResult.logs);
@@ -337993,7 +338361,7 @@ var require_package4 = __commonJS({
337993
338361
  "package.json"(exports, module) {
337994
338362
  module.exports = {
337995
338363
  name: "@validate.qa/runner",
337996
- version: "1.0.43",
338364
+ version: "1.0.44",
337997
338365
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
337998
338366
  bin: {
337999
338367
  "validate-runner": "dist/cli.js"
@@ -338111,32 +338479,24 @@ var import_runner_core12 = __toESM(require_dist2());
338111
338479
 
338112
338480
  // src/services/auth-state.ts
338113
338481
  init_dist();
338114
- var INLINE_LOGIN_TAG = "@inline-login";
338115
338482
  var GUARDED_LOGIN_TAG = "@auth-guarded";
338116
338483
  function sharedAuthInjectEnabled() {
338117
338484
  return isSharedAuthInjectEnabled();
338118
338485
  }
338119
- function hasGeneratedAuthGuard(playwrightCode) {
338486
+ function hasGeneratedAuthGuard2(playwrightCode) {
338120
338487
  if (!playwrightCode) return false;
338121
338488
  return /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
338122
338489
  }
338123
338490
  function shouldCaptureAuthState(tags, playwrightCode) {
338124
- return sharedAuthInjectEnabled() && Array.isArray(tags) && tags.includes("@needs-auth") && tags.includes(GUARDED_LOGIN_TAG) && !tags.includes("@auth") && hasGeneratedAuthGuard(playwrightCode);
338125
- }
338126
- function hasInlineLoginSteps(playwrightCode) {
338127
- if (!playwrightCode) return false;
338128
- return /process\.env\.TEST_USER_EMAIL!?/.test(playwrightCode) && /process\.env\.TEST_USER_PASSWORD!?/.test(playwrightCode);
338491
+ return sharedAuthInjectEnabled() && Array.isArray(tags) && tags.includes("@needs-auth") && tags.includes(GUARDED_LOGIN_TAG) && !tags.includes("@auth") && hasGeneratedAuthGuard2(playwrightCode);
338129
338492
  }
338130
338493
  function shouldUseAuthState(tags, playwrightCode) {
338131
338494
  if (!sharedAuthInjectEnabled()) return false;
338132
338495
  if (!Array.isArray(tags)) return false;
338133
- if (tags.includes("@auth")) return false;
338134
- if (tags.includes("@api")) return true;
338135
- if (!tags.includes("@needs-auth")) return false;
338136
- if (tags.includes(GUARDED_LOGIN_TAG) && hasGeneratedAuthGuard(playwrightCode)) return true;
338137
- const isGeneratedVariant = tags.includes("@edge") || tags.includes("@bug");
338138
- const hasLegacyInlineLogin = tags.includes("@discovered") && !isGeneratedVariant && hasInlineLoginSteps(playwrightCode);
338139
- return !tags.includes(INLINE_LOGIN_TAG) && !hasLegacyInlineLogin;
338496
+ return authPlanFor(tags, playwrightCode).needsCookieState;
338497
+ }
338498
+ function authPlanFor(tags, playwrightCode) {
338499
+ return deriveAuthPlan(tags, playwrightCode, { sharedAuthInjectEnabled: sharedAuthInjectEnabled() });
338140
338500
  }
338141
338501
 
338142
338502
  // src/version-check.ts
@@ -345535,7 +345895,18 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
345535
345895
  requestLiveBrowserHeartbeat();
345536
345896
  }, 5e3);
345537
345897
  liveBrowserHeartbeatInterval.unref?.();
345898
+ const authPlan = authPlanFor(run2.tags, run2.playwrightCode);
345899
+ const runAuthCachePath = (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
345900
+ projectId: run2.projectId || opts.projectId || "default",
345901
+ serverUrl: opts.serverUrl,
345902
+ baseUrl: run2.baseUrl,
345903
+ testVariables: run2.testVariables,
345904
+ authToken: opts.authToken
345905
+ }, run2.runId).cachePath;
345906
+ const wrapperStorageStatePath = authPlan.needsCookieState && runMode !== "auth-setup" ? (0, import_runner_core12.getValidLocalAuthState)(runAuthCachePath) : void 0;
345538
345907
  const runNativeWithLiveBrowser = (nativeOptions) => (0, import_runner_core.executeTestViaNativePlaywright)({
345908
+ authStandingMode: authPlan.standingHeadersMode,
345909
+ storageStatePath: wrapperStorageStatePath,
345539
345910
  ...nativeOptions,
345540
345911
  liveBrowserHandle,
345541
345912
  onLiveBrowserSnapshot: requestLiveBrowserHeartbeat
@@ -345563,11 +345934,24 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
345563
345934
  if (!run2.baseUrl) {
345564
345935
  preflightError = "Pre-flight failed: Missing BASE_URL. Configure it in Test Data \u2192 Test Variables.";
345565
345936
  } else {
345566
- const needsAuth = Array.isArray(run2.tags) && run2.tags.some((t) => t === "@auth" || t === "@needs-auth" || t === "@api");
345567
- if (needsAuth && !run2.testVariables?.["TEST_USER_EMAIL"]) {
345568
- preflightError = "Pre-flight failed: Missing TEST_USER_EMAIL. Tests tagged @auth, @needs-auth, or @api require login credentials.";
345569
- } else if (needsAuth && !run2.testVariables?.["TEST_USER_PASSWORD"]) {
345570
- preflightError = "Pre-flight failed: Missing TEST_USER_PASSWORD. Tests tagged @auth, @needs-auth, or @api require login credentials.";
345937
+ const validStanding = (0, import_runner_core13.parseApiAuthHeaders)(run2.testVariables?.["API_AUTH_HEADERS"]) !== void 0;
345938
+ if (authPlan.configurationError) {
345939
+ preflightError = `Pre-flight failed: ${authPlan.configurationError}. Fix the test's auth-type tag before running.`;
345940
+ } else if (authPlan.needsStandingHeaders && !validStanding) {
345941
+ preflightError = "Pre-flight failed: This @auth-standing @api test requires a valid API_AUTH_HEADERS JSON object (header name \u2192 value).";
345942
+ } else {
345943
+ const legacyHeaderBypass = authPlan.source === "legacy" && authPlan.standingHeadersMode === "legacy-if-configured" && validStanding;
345944
+ const code = run2.playwrightCode ?? "";
345945
+ const codeProvided = code.trim().length > 0;
345946
+ const codeUsesCreds = codeProvided && /TEST_USER_EMAIL|TEST_USER_PASSWORD|__vqaLogin/.test(code);
345947
+ const tags = Array.isArray(run2.tags) ? run2.tags : [];
345948
+ const isHallucinationShape = codeProvided && !codeUsesCreds && tags.includes("@inline-login") && !tags.includes("@api");
345949
+ const requireCreds = authPlan.needsLoginCreds && !legacyHeaderBypass && !isHallucinationShape;
345950
+ if (requireCreds && !run2.testVariables?.["TEST_USER_EMAIL"]?.trim()) {
345951
+ preflightError = "Pre-flight failed: Missing TEST_USER_EMAIL. Tests that log in (tagged @auth, @needs-auth, @api, @auth-cookie-state, @auth-login-cookie, or @auth-login-bearer) require login credentials.";
345952
+ } else if (requireCreds && !run2.testVariables?.["TEST_USER_PASSWORD"]?.trim()) {
345953
+ preflightError = "Pre-flight failed: Missing TEST_USER_PASSWORD. Tests that log in (tagged @auth, @needs-auth, @api, @auth-cookie-state, @auth-login-cookie, or @auth-login-bearer) require login credentials.";
345954
+ }
345571
345955
  }
345572
345956
  }
345573
345957
  if (preflightError) {
@@ -346176,10 +346560,13 @@ ${finalVerifyResult.logs ?? ""}`;
346176
346560
  });
346177
346561
  if (healRes.ok) {
346178
346562
  const { healedCode } = await healRes.json();
346563
+ const failedTestPlan = authPlanFor(failedTest.tags, healedCode);
346179
346564
  const verifyResult = await runNativeWithLiveBrowser({
346180
346565
  playwrightCode: healedCode,
346181
346566
  baseUrl: run2.baseUrl,
346182
346567
  testVariables: run2.testVariables,
346568
+ storageStatePath: failedTestPlan.needsCookieState ? (0, import_runner_core12.getValidLocalAuthState)(runAuthCachePath) : void 0,
346569
+ authStandingMode: failedTestPlan.standingHeadersMode,
346183
346570
  validateContext: callbackContext
346184
346571
  });
346185
346572
  results.push({
@@ -347663,6 +348050,10 @@ ${result2.logs}`);
347663
348050
  log.warn(`\u26A0 Rate-limit block detected - posting ERROR (not FAILED) to skip heal/quarantine`);
347664
348051
  }
347665
348052
  }
348053
+ if (runStatus === "FAILED" && classifyErrorMessage(runError ?? null) === "config_issue") {
348054
+ runStatus = "ERROR";
348055
+ log.warn(`\u26A0 CONFIG_ISSUE from native execution - posting ERROR (not FAILED) to skip heal/quarantine`);
348056
+ }
347666
348057
  await postResult(opts.serverUrl, headers, run2.runId, {
347667
348058
  status: runStatus,
347668
348059
  stepResults: result.stepResults,
@@ -347678,7 +348069,7 @@ ${result2.logs}`);
347678
348069
  if (result.passed) {
347679
348070
  log.ok(`PASSED (${duration}ms)${result.suspiciousPass ? ` \u26A0 suspicious-pass (viewport ${Math.round(result.suspiciousPass.emptyRatio * 100)}% blank)` : ""}`);
347680
348071
  } else if (runStatus === "ERROR") {
347681
- log.warn(`BLOCKED (${duration}ms) - rate-limit detected, not counted as failure`);
348072
+ log.warn(`BLOCKED (${duration}ms) - environmental block (rate-limit / config-issue), not counted as failure`);
347682
348073
  } else {
347683
348074
  log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
347684
348075
  if (result.logs) {