@validate.qa/runner 1.0.42 → 1.0.43

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 +914 -131
  2. package/dist/cli.mjs +914 -131
  3. package/package.json +2 -2
package/dist/cli.mjs CHANGED
@@ -1761,7 +1761,18 @@ var init_constants = __esm({
1761
1761
  "MINIMAX_API_KEY",
1762
1762
  "ANTHROPIC_API_KEY",
1763
1763
  "NVIDIA_API_KEY",
1764
- "DEEPSEEK_API_KEY"
1764
+ "DEEPSEEK_API_KEY",
1765
+ // Run-scoped callback capabilities. The parent runner injects these after
1766
+ // sanitization; customer variables must never impersonate the control plane.
1767
+ "AUTH_FILE",
1768
+ "SHARED_AUTH_INJECT",
1769
+ "VALIDATE_RUN_ID",
1770
+ "VALIDATE_CLAIM_EPOCH",
1771
+ "VALIDATE_AUTH_CONFIG_HASH",
1772
+ "VALIDATE_TASK_ARN",
1773
+ "VALIDATE_RUNNER_AUTH",
1774
+ "VALIDATE_SERVER_URL",
1775
+ "VALIDATE_LIVE_BROWSER_STATE_PATH"
1765
1776
  ]);
1766
1777
  SPAWNED_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
1767
1778
  // Runtime essentials - Node.js and Playwright need these to function
@@ -5029,6 +5040,16 @@ var init_voice_config = __esm({
5029
5040
  }
5030
5041
  });
5031
5042
 
5043
+ // ../shared/dist/feature-flags.js
5044
+ function isSharedAuthInjectEnabled(value = process.env.SHARED_AUTH_INJECT) {
5045
+ return value?.trim().toLowerCase() !== "false";
5046
+ }
5047
+ var init_feature_flags = __esm({
5048
+ "../shared/dist/feature-flags.js"() {
5049
+ "use strict";
5050
+ }
5051
+ });
5052
+
5032
5053
  // ../shared/dist/index.js
5033
5054
  var dist_exports = {};
5034
5055
  __export(dist_exports, {
@@ -5152,6 +5173,7 @@ __export(dist_exports, {
5152
5173
  isRunnableStep: () => isRunnableStep,
5153
5174
  isSafeDeepLink: () => isSafeDeepLink,
5154
5175
  isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
5176
+ isSharedAuthInjectEnabled: () => isSharedAuthInjectEnabled,
5155
5177
  isSttModelForProvider: () => isSttModelForProvider,
5156
5178
  isTestReadyForPlatform: () => isTestReadyForPlatform,
5157
5179
  isTrialExpired: () => isTrialExpired,
@@ -5218,6 +5240,7 @@ var init_dist = __esm({
5218
5240
  init_test_counting();
5219
5241
  init_origin_domain();
5220
5242
  init_voice_config();
5243
+ init_feature_flags();
5221
5244
  }
5222
5245
  });
5223
5246
 
@@ -10681,7 +10704,7 @@ var require_transport = __commonJS({
10681
10704
  "../../node_modules/pino/lib/transport.js"(exports, module) {
10682
10705
  "use strict";
10683
10706
  var { createRequire } = __require("module");
10684
- var { existsSync: existsSync4 } = __require("fs");
10707
+ var { existsSync: existsSync3 } = __require("fs");
10685
10708
  var getCallers = require_caller();
10686
10709
  var { join: join7, isAbsolute, sep } = __require("path");
10687
10710
  var { fileURLToPath } = __require("url");
@@ -10755,7 +10778,7 @@ var require_transport = __commonJS({
10755
10778
  return false;
10756
10779
  }
10757
10780
  }
10758
- return isAbsolute(path) && !existsSync4(path);
10781
+ return isAbsolute(path) && !existsSync3(path);
10759
10782
  }
10760
10783
  function stripQuotes(value) {
10761
10784
  const first = value[0];
@@ -76170,6 +76193,126 @@ export const test = base.extend({
76170
76193
  }
76171
76194
  });
76172
76195
 
76196
+ // ../runner-core/dist/services/run-callback-proxy.js
76197
+ var require_run_callback_proxy = __commonJS({
76198
+ "../runner-core/dist/services/run-callback-proxy.js"(exports) {
76199
+ "use strict";
76200
+ Object.defineProperty(exports, "__esModule", { value: true });
76201
+ exports.startRunCallbackProxy = startRunCallbackProxy;
76202
+ var node_crypto_1 = __require("crypto");
76203
+ var node_http_1 = __require("http");
76204
+ var MAX_CALLBACK_BODY_BYTES = 5 * 1024 * 1024;
76205
+ var CALLBACK_TIMEOUT_MS = 3e4;
76206
+ function sendJson(res, status, body) {
76207
+ const payload = JSON.stringify(body);
76208
+ res.writeHead(status, {
76209
+ "cache-control": "no-store",
76210
+ "content-type": "application/json",
76211
+ "content-length": Buffer.byteLength(payload)
76212
+ });
76213
+ res.end(payload);
76214
+ }
76215
+ async function readRequestBody(req) {
76216
+ const chunks = [];
76217
+ let total = 0;
76218
+ for await (const chunk of req) {
76219
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
76220
+ total += buffer.length;
76221
+ if (total > MAX_CALLBACK_BODY_BYTES)
76222
+ throw new Error("callback body too large");
76223
+ chunks.push(buffer);
76224
+ }
76225
+ return Buffer.concat(chunks);
76226
+ }
76227
+ async function startRunCallbackProxy(input) {
76228
+ const upstream = new URL(input.serverUrl);
76229
+ if (upstream.protocol !== "http:" && upstream.protocol !== "https:") {
76230
+ throw new Error("callback server URL must use http or https");
76231
+ }
76232
+ const runPath = `/api/runner/runs/${encodeURIComponent(input.runId)}`;
76233
+ const allowedPaths = /* @__PURE__ */ new Set([`${runPath}/credential`, `${runPath}/auth-state`]);
76234
+ const token = (0, node_crypto_1.randomBytes)(32).toString("base64url");
76235
+ const authHeader = `Bearer ${token}`;
76236
+ const server3 = (0, node_http_1.createServer)(async (req, res) => {
76237
+ try {
76238
+ const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
76239
+ if (req.method !== "POST" || !allowedPaths.has(requestUrl.pathname) || requestUrl.search) {
76240
+ sendJson(res, 404, { error: "Callback route not available" });
76241
+ return;
76242
+ }
76243
+ if (req.headers.authorization !== authHeader) {
76244
+ sendJson(res, 401, { error: "Invalid callback capability" });
76245
+ return;
76246
+ }
76247
+ const body = await readRequestBody(req);
76248
+ let parsedBody;
76249
+ try {
76250
+ const parsed = JSON.parse(body.toString("utf8"));
76251
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
76252
+ throw new Error("invalid body");
76253
+ parsedBody = parsed;
76254
+ } catch {
76255
+ sendJson(res, 400, { error: "Callback body must be a JSON object" });
76256
+ return;
76257
+ }
76258
+ const upstreamBody = JSON.stringify({
76259
+ ...parsedBody,
76260
+ ...input.taskArn ? { taskArn: input.taskArn } : {},
76261
+ ...input.claimEpoch !== void 0 ? { claimEpoch: input.claimEpoch } : {},
76262
+ ...input.authConfigHash ? { authConfigHash: input.authConfigHash } : {}
76263
+ });
76264
+ const destination = new URL(requestUrl.pathname, upstream);
76265
+ const response = await fetch(destination, {
76266
+ method: "POST",
76267
+ headers: {
76268
+ Authorization: input.upstreamAuthHeader,
76269
+ "Content-Type": "application/json"
76270
+ },
76271
+ body: upstreamBody,
76272
+ redirect: "error",
76273
+ signal: AbortSignal.timeout(CALLBACK_TIMEOUT_MS)
76274
+ });
76275
+ const responseBody = Buffer.from(await response.arrayBuffer());
76276
+ res.writeHead(response.status, {
76277
+ "cache-control": "no-store",
76278
+ "content-type": response.headers.get("content-type") ?? "application/json",
76279
+ "content-length": responseBody.length
76280
+ });
76281
+ res.end(responseBody);
76282
+ } catch (error2) {
76283
+ const message = error2 instanceof Error && error2.message === "callback body too large" ? error2.message : "Callback forwarding failed";
76284
+ sendJson(res, message === "callback body too large" ? 413 : 502, { error: message });
76285
+ }
76286
+ });
76287
+ server3.on("clientError", (_error, socket) => socket.destroy());
76288
+ await new Promise((resolve2, reject) => {
76289
+ const onError = (error2) => reject(error2);
76290
+ server3.once("error", onError);
76291
+ server3.listen(0, "127.0.0.1", () => {
76292
+ server3.off("error", onError);
76293
+ resolve2();
76294
+ });
76295
+ });
76296
+ const address = server3.address();
76297
+ if (!address) {
76298
+ server3.close();
76299
+ throw new Error("callback proxy failed to bind");
76300
+ }
76301
+ let closed = false;
76302
+ return {
76303
+ serverUrl: `http://127.0.0.1:${address.port}`,
76304
+ authHeader,
76305
+ close: async () => {
76306
+ if (closed)
76307
+ return;
76308
+ closed = true;
76309
+ await new Promise((resolve2) => server3.close(() => resolve2()));
76310
+ }
76311
+ };
76312
+ }
76313
+ }
76314
+ });
76315
+
76173
76316
  // ../runner-core/dist/services/playwright-native.js
76174
76317
  var require_playwright_native = __commonJS({
76175
76318
  "../runner-core/dist/services/playwright-native.js"(exports) {
@@ -76214,6 +76357,7 @@ var require_playwright_native = __commonJS({
76214
76357
  Object.defineProperty(exports, "__esModule", { value: true });
76215
76358
  exports.scrubHarRequestBody = exports.buildOriginScopedApiFixtures = exports.isAppOriginRequest = exports.resolveApiAuthHeaders = exports.parseApiAuthHeaders = void 0;
76216
76359
  exports.executeTestViaNativePlaywright = executeTestViaNativePlaywright2;
76360
+ exports.buildNativePlaywrightChildEnv = buildNativePlaywrightChildEnv;
76217
76361
  exports.parseJsonResults = parseJsonResults2;
76218
76362
  exports.parseHarToApiSummary = parseHarToApiSummary;
76219
76363
  var promises_1 = __require("fs/promises");
@@ -76225,6 +76369,7 @@ var require_playwright_native = __commonJS({
76225
76369
  var api_call_redaction_js_1 = require_api_call_redaction();
76226
76370
  var har_stream_js_1 = require_har_stream();
76227
76371
  var api_auth_headers_js_1 = require_api_auth_headers();
76372
+ var run_callback_proxy_js_1 = require_run_callback_proxy();
76228
76373
  var api_auth_headers_js_2 = require_api_auth_headers();
76229
76374
  Object.defineProperty(exports, "parseApiAuthHeaders", { enumerable: true, get: function() {
76230
76375
  return api_auth_headers_js_2.parseApiAuthHeaders;
@@ -76247,7 +76392,7 @@ var require_playwright_native = __commonJS({
76247
76392
  var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
76248
76393
  var PW_CLI = __require.resolve("@playwright/test/cli");
76249
76394
  async function executeTestViaNativePlaywright2(options) {
76250
- const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
76395
+ const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
76251
76396
  const logs2 = [];
76252
76397
  const screenshots = [];
76253
76398
  logs2.push(`\u{1F3AD} Native Playwright starting`);
@@ -76257,6 +76402,7 @@ var require_playwright_native = __commonJS({
76257
76402
  logs2.push("\u26A0\uFE0F WARNING: baseUrl is empty - test will likely fail with navigation errors");
76258
76403
  }
76259
76404
  let runDir = null;
76405
+ let callbackProxy;
76260
76406
  try {
76261
76407
  runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
76262
76408
  await (0, promises_1.mkdir)(runDir, { recursive: true });
@@ -76516,35 +76662,51 @@ module.exports = {
76516
76662
  const configPath = (0, path_1.join)(runDir, "playwright.config.js");
76517
76663
  await (0, promises_1.writeFile)(configPath, config);
76518
76664
  const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(runDir);
76519
- const envWithIsolation = {
76520
- ...testVariables ?? {},
76665
+ const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
76666
+ const callbackClaimEpoch = Number.isSafeInteger(validateContext?.claimEpoch) ? validateContext?.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
76667
+ if (validateContext?.runId && validateContext.serverUrl && validateContext.authHeader) {
76668
+ try {
76669
+ callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
76670
+ runId: validateContext.runId,
76671
+ serverUrl: validateContext.serverUrl,
76672
+ upstreamAuthHeader: validateContext.authHeader,
76673
+ taskArn: process.env.VALIDATE_TASK_ARN,
76674
+ claimEpoch: callbackClaimEpoch,
76675
+ authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
76676
+ });
76677
+ } catch (error3) {
76678
+ logs2.push(`Callback proxy unavailable: ${error3 instanceof Error ? error3.message : String(error3)}`);
76679
+ }
76680
+ }
76681
+ const childTestVariables = { ...testVariables ?? {} };
76682
+ const trustedRuntimeEnv = {
76521
76683
  ...isolation.env,
76522
- // persistCredential() callback envs — set only when the runner supplied
76523
- // validateContext. Without these the helper inside the test no-ops with
76524
- // a warning, which is the correct behavior when the test is being run
76525
- // manually (outside the validate.qa harness).
76526
- ...validateContext?.runId ? { VALIDATE_RUN_ID: validateContext.runId } : {},
76527
- ...validateContext?.serverUrl ? { VALIDATE_SERVER_URL: validateContext.serverUrl } : {},
76528
- ...validateContext?.authHeader ? { VALIDATE_RUNNER_AUTH: validateContext.authHeader } : {},
76684
+ // Generated callbacks receive only a random loopback capability. The global
76685
+ // runner bearer token and real control-plane URL stay in the parent process.
76686
+ ...callbackProxy && validateContext?.runId ? {
76687
+ VALIDATE_RUN_ID: validateContext.runId,
76688
+ ...callbackClaimEpoch !== void 0 ? { VALIDATE_CLAIM_EPOCH: String(callbackClaimEpoch) } : {},
76689
+ VALIDATE_SERVER_URL: callbackProxy.serverUrl,
76690
+ VALIDATE_RUNNER_AUTH: callbackProxy.authHeader
76691
+ } : {},
76529
76692
  // Owning Fargate taskArn (a per-task constant set once at task boot in
76530
76693
  // execute-one.ts; absent on the local/Railway runner). Read from
76531
76694
  // process.env rather than validateContext since it's process-wide, not
76532
76695
  // per-run. Lets persistCredential() prove ownership to /credential.
76533
76696
  ...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {},
76697
+ ...authStateCapturePath ? { AUTH_FILE: authStateCapturePath } : {},
76534
76698
  ...liveBrowserStatePath ? { VALIDATE_LIVE_BROWSER_STATE_PATH: liveBrowserStatePath } : {},
76535
- // Shared-auth kill-switch (SHARED_AUTH_INJECT) must reach the child: the stamped
76536
- // __vqaLogin's early-publish POST is gated on it at RUNTIME. buildSandboxedEnv only
76537
- // copies an allowlist from process.env (SHARED_AUTH_INJECT is not on it), so without
76538
- // this forward the flag reads as undefined in the test and the early publish is dead
76539
- // code. Forwarded (not allowlisted) so it stays a per-run toggle; absent → inert.
76699
+ // Preserve an explicit shared-auth kill-switch in the child. Unset defaults on in
76700
+ // both parent and generated __vqaLogin code; "false" must still cross this process
76701
+ // boundary to disable the early publish POST. It remains a per-run toggle.
76540
76702
  ...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
76541
76703
  };
76542
- delete envWithIsolation.API_AUTH_HEADERS;
76704
+ delete childTestVariables.API_AUTH_HEADERS;
76543
76705
  logs2.push("\u{1F680} Spawning playwright test...");
76544
76706
  const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
76545
76707
  let playResult;
76546
76708
  try {
76547
- playResult = await runPlaywright(configPath, envWithIsolation);
76709
+ playResult = await runPlaywright(configPath, childTestVariables, trustedRuntimeEnv);
76548
76710
  } finally {
76549
76711
  await stopLiveBrowserPolling?.();
76550
76712
  }
@@ -76628,6 +76790,8 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76628
76790
  error: msg
76629
76791
  };
76630
76792
  } finally {
76793
+ await callbackProxy?.close().catch(() => {
76794
+ });
76631
76795
  if (runDir) {
76632
76796
  await (0, promises_1.rm)(runDir, { recursive: true, force: true }).catch(() => {
76633
76797
  });
@@ -76679,9 +76843,9 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76679
76843
  stopped = true;
76680
76844
  };
76681
76845
  }
76682
- function runPlaywright(configPath, extraEnv) {
76846
+ function runPlaywright(configPath, testVariables, trustedRuntimeEnv) {
76683
76847
  return new Promise((resolvePromise) => {
76684
- const sandboxedEnv = (0, shared_1.buildSandboxedEnv)(process.env, extraEnv);
76848
+ const sandboxedEnv = buildNativePlaywrightChildEnv(process.env, testVariables, trustedRuntimeEnv);
76685
76849
  const proc = (0, spawn_with_tree_kill_js_1.spawnDetachedGroup)(process.execPath, [PW_CLI, "test", "--config", configPath], {
76686
76850
  cwd: PACKAGE_DIR,
76687
76851
  env: sandboxedEnv,
@@ -76724,6 +76888,12 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76724
76888
  });
76725
76889
  });
76726
76890
  }
76891
+ function buildNativePlaywrightChildEnv(processEnv, testVariables, trustedRuntimeEnv) {
76892
+ return {
76893
+ ...(0, shared_1.buildSandboxedEnv)(processEnv, testVariables),
76894
+ ...trustedRuntimeEnv ?? {}
76895
+ };
76896
+ }
76727
76897
  async function collectScreenshots(dir, screenshots) {
76728
76898
  let entries;
76729
76899
  try {
@@ -80445,10 +80615,12 @@ var require_auth_recipe = __commonJS({
80445
80615
  exports.upsertAuthBlock = upsertAuthBlock;
80446
80616
  exports.buildHealAuthStamp = buildHealAuthStamp;
80447
80617
  var crypto_1 = __require("crypto");
80618
+ var shared_1 = (init_dist(), __toCommonJS(dist_exports));
80448
80619
  var discovery_lint_flags_js_1 = require_discovery_lint_flags();
80449
- exports.AUTH_MARKER_VERSION = "v1";
80620
+ exports.AUTH_MARKER_VERSION = "v3";
80450
80621
  var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
80451
80622
  var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports.AUTH_MARKER_VERSION} <<<`;
80623
+ var SHARED_AUTH_RUNTIME_ENABLED = `!/^false$/i.test((process.env.SHARED_AUTH_INJECT ?? '').trim())`;
80452
80624
  var AUTH_REGION_RE = /\/\/ >>> VALIDATE_QA_AUTH[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH[^\n]*\r?\n?/g;
80453
80625
  function computeRecipeHash(input) {
80454
80626
  const canonical = JSON.stringify({
@@ -80526,16 +80698,18 @@ var require_auth_recipe = __commonJS({
80526
80698
  if (opts?.warmGuard) {
80527
80699
  const warmLanding = recipe.redirectRoute ?? (recipe.authProof.kind === "url" ? recipe.authProof.route : recipe.preNavRoute);
80528
80700
  const loginRoute = recipe.route ?? recipe.preNavRoute;
80529
- lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
80701
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED}) {`);
80702
+ lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
80530
80703
  if (recipe.authProof.kind === "url") {
80531
80704
  const pred = urlProofPredicateExpr(recipe.authProof.route);
80532
- lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
80533
- lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
80705
+ lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
80706
+ lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
80534
80707
  } else if (loginRoute) {
80535
- lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
80708
+ lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
80536
80709
  } else {
80537
- lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
80710
+ lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
80538
80711
  }
80712
+ lines.push(` }`);
80539
80713
  }
80540
80714
  lines.push(` await page.goto(${JSON.stringify(recipe.preNavRoute)}, { waitUntil: 'domcontentloaded' });`);
80541
80715
  for (const cmd of recipe.preNavCmds)
@@ -80548,11 +80722,11 @@ var require_auth_recipe = __commonJS({
80548
80722
  lines.push(renderAuthUrlWaitLine(recipe.redirectRoute));
80549
80723
  lines.push(` ${renderAuthProofAssertion(recipe.authProof)}`);
80550
80724
  if (opts?.warmGuard) {
80551
- lines.push(` if (process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
80552
- lines.push(` if (process.env.SHARED_AUTH_INJECT === 'true' && process.env.VALIDATE_TASK_ARN && process.env.VALIDATE_SERVER_URL && process.env.VALIDATE_RUN_ID) {`);
80725
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
80726
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.VALIDATE_TASK_ARN && process.env.VALIDATE_CLAIM_EPOCH && process.env.VALIDATE_SERVER_URL && process.env.VALIDATE_RUN_ID) {`);
80553
80727
  lines.push(` try {`);
80554
80728
  lines.push(` const __vqaState = await page.context().storageState();`);
80555
- lines.push(` await page.request.post(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + process.env.VALIDATE_RUN_ID + '/auth-state', { headers: { Authorization: process.env.VALIDATE_RUNNER_AUTH || '' }, data: { storageState: JSON.stringify(__vqaState), taskArn: process.env.VALIDATE_TASK_ARN }, timeout: 10000 });`);
80729
+ lines.push(` await page.request.post(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + process.env.VALIDATE_RUN_ID + '/auth-state', { headers: { Authorization: process.env.VALIDATE_RUNNER_AUTH || '' }, data: { storageState: JSON.stringify(__vqaState), taskArn: process.env.VALIDATE_TASK_ARN, claimEpoch: Number(process.env.VALIDATE_CLAIM_EPOCH) }, timeout: 10000 });`);
80556
80730
  lines.push(` } catch {}`);
80557
80731
  lines.push(` }`);
80558
80732
  }
@@ -80564,6 +80738,8 @@ var require_auth_recipe = __commonJS({
80564
80738
  "AUTH_FILE",
80565
80739
  "SHARED_AUTH_INJECT",
80566
80740
  "VALIDATE_RUN_ID",
80741
+ "VALIDATE_CLAIM_EPOCH",
80742
+ "VALIDATE_AUTH_CONFIG_HASH",
80567
80743
  "VALIDATE_TASK_ARN",
80568
80744
  "VALIDATE_RUNNER_AUTH",
80569
80745
  "VALIDATE_SERVER_URL"
@@ -80736,7 +80912,7 @@ ${tail}`;
80736
80912
  let result = stripAuthRegions(code);
80737
80913
  result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
80738
80914
  result = ensureExpectImported(result);
80739
- const warmGuard = process.env.SHARED_AUTH_INJECT === "true";
80915
+ const warmGuard = (0, shared_1.isSharedAuthInjectEnabled)();
80740
80916
  result = insertAfterPwImport(result, renderAuthFn(ctx.recipe, { warmGuard }));
80741
80917
  result = ensureLoginCall(result);
80742
80918
  const n = countAuthRegions(result);
@@ -85866,9 +86042,9 @@ ${testOpen}`;
85866
86042
  basePath: options?.basePath
85867
86043
  });
85868
86044
  const leakedSensitiveKeys = findSensitiveLiteralLeaks(playwrightCode, options?.sensitiveEnvMap);
85869
- const authGuardStamped = !!options?.authRecipe && rawTags.includes("@needs-auth") && !rawTags.includes("@auth") && process.env.SHARED_AUTH_INJECT === "true";
85870
- const baseTags = ensureDiscoveredTag(Array.isArray(test.tags) ? test.tags : ["@discovered", "@regression"]);
85871
- const tags = authGuardStamped && !baseTags.includes("@auth-guarded") ? [...baseTags, "@auth-guarded"] : baseTags;
86045
+ const authGuardStamped = !!options?.authRecipe && rawTags.includes("@needs-auth") && !rawTags.includes("@auth") && (0, shared_1.isSharedAuthInjectEnabled)() && /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
86046
+ const baseTags = ensureDiscoveredTag(Array.isArray(test.tags) ? test.tags : ["@discovered", "@regression"]).filter((tag) => tag !== "@auth-guarded");
86047
+ const tags = authGuardStamped ? [...baseTags, "@auth-guarded"] : baseTags;
85872
86048
  return {
85873
86049
  name: String(test.name || "Unnamed test").slice(0, 200),
85874
86050
  description: String(test.description || test.name || "").slice(0, 500),
@@ -99683,7 +99859,7 @@ ${lanes.join("\n")}
99683
99859
  return process.memoryUsage().heapUsed;
99684
99860
  },
99685
99861
  getFileSize(path) {
99686
- const stat = statSync2(path);
99862
+ const stat = statSync(path);
99687
99863
  if (stat == null ? void 0 : stat.isFile()) {
99688
99864
  return stat.size;
99689
99865
  }
@@ -99727,7 +99903,7 @@ ${lanes.join("\n")}
99727
99903
  }
99728
99904
  };
99729
99905
  return nodeSystem;
99730
- function statSync2(path) {
99906
+ function statSync(path) {
99731
99907
  try {
99732
99908
  return _fs.statSync(path, statSyncOptions);
99733
99909
  } catch {
@@ -99786,7 +99962,7 @@ ${lanes.join("\n")}
99786
99962
  activeSession.post("Profiler.stop", (err, { profile }) => {
99787
99963
  var _a;
99788
99964
  if (!err) {
99789
- if ((_a = statSync2(profilePath)) == null ? void 0 : _a.isDirectory()) {
99965
+ if ((_a = statSync(profilePath)) == null ? void 0 : _a.isDirectory()) {
99790
99966
  profilePath = _path.join(profilePath, `${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`);
99791
99967
  }
99792
99968
  try {
@@ -99906,7 +100082,7 @@ ${lanes.join("\n")}
99906
100082
  let stat;
99907
100083
  if (typeof dirent === "string" || dirent.isSymbolicLink()) {
99908
100084
  const name = combinePaths(path, entry);
99909
- stat = statSync2(name);
100085
+ stat = statSync(name);
99910
100086
  if (!stat) {
99911
100087
  continue;
99912
100088
  }
@@ -99930,7 +100106,7 @@ ${lanes.join("\n")}
99930
100106
  return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames2, process.cwd(), depth, getAccessibleFileSystemEntries, realpath);
99931
100107
  }
99932
100108
  function fileSystemEntryExists(path, entryKind) {
99933
- const stat = statSync2(path);
100109
+ const stat = statSync(path);
99934
100110
  if (!stat) {
99935
100111
  return false;
99936
100112
  }
@@ -99972,7 +100148,7 @@ ${lanes.join("\n")}
99972
100148
  }
99973
100149
  function getModifiedTime3(path) {
99974
100150
  var _a;
99975
- return (_a = statSync2(path)) == null ? void 0 : _a.mtime;
100151
+ return (_a = statSync(path)) == null ? void 0 : _a.mtime;
99976
100152
  }
99977
100153
  function setModifiedTime(path, time) {
99978
100154
  try {
@@ -308483,6 +308659,7 @@ var require_regression_runner = __commonJS({
308483
308659
  var chromium_isolation_js_1 = require_chromium_isolation();
308484
308660
  var browser_config_js_1 = require_browser_config();
308485
308661
  var spawn_with_tree_kill_js_1 = require_spawn_with_tree_kill();
308662
+ var run_callback_proxy_js_1 = require_run_callback_proxy();
308486
308663
  var SUITE_SPAWN_TIMEOUT_MS = 85 * 60 * 1e3;
308487
308664
  var PACKAGE_DIR = (0, path_1.resolve)(__dirname, "..", "..");
308488
308665
  var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
@@ -308497,6 +308674,7 @@ var require_regression_runner = __commonJS({
308497
308674
  const tarPath = (0, path_1.join)(RUNS_DIR, `regression-${runId}.tar.gz`);
308498
308675
  const startTime = Date.now();
308499
308676
  let logs2 = "";
308677
+ let callbackProxy;
308500
308678
  try {
308501
308679
  await (0, promises_1.mkdir)(extractDir, { recursive: true });
308502
308680
  const tarballUrl = `https://api.github.com/repos/${repoOwner}/${repoName}/tarball/${repoBranch}`;
@@ -308526,19 +308704,40 @@ var require_regression_runner = __commonJS({
308526
308704
  await (0, promises_1.writeFile)((0, path_1.join)(extractDir, ".env"), envContent, "utf-8");
308527
308705
  const jsonResultsPath = (0, path_1.join)(extractDir, "test-results", "results.json");
308528
308706
  const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(extractDir);
308707
+ const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
308708
+ const callbackClaimEpoch = Number.isSafeInteger(opts.claimEpoch) ? opts.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
308709
+ if (opts.validateServerUrl && opts.validateRunnerAuthHeader) {
308710
+ try {
308711
+ callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
308712
+ runId,
308713
+ serverUrl: opts.validateServerUrl,
308714
+ upstreamAuthHeader: opts.validateRunnerAuthHeader,
308715
+ taskArn: process.env.VALIDATE_TASK_ARN,
308716
+ claimEpoch: callbackClaimEpoch,
308717
+ authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
308718
+ });
308719
+ } catch (error2) {
308720
+ logs2 += `Callback proxy unavailable: ${error2 instanceof Error ? error2.message : String(error2)}
308721
+ `;
308722
+ }
308723
+ }
308529
308724
  const runEnv = {
308530
308725
  ...(0, shared_1.buildSandboxedEnv)(process.env, testVariables),
308531
308726
  ...isolation.env,
308532
308727
  CI: "true",
308533
308728
  PLAYWRIGHT_JSON_OUTPUT_NAME: jsonResultsPath,
308534
- // Callback envs for tests/setup/credentials.ts → persistCredential.
308535
- // Tests that rotate a credential POST back to the server using these.
308536
- VALIDATE_RUN_ID: runId,
308537
- ...opts.validateServerUrl ? { VALIDATE_SERVER_URL: opts.validateServerUrl } : {},
308538
- ...opts.validateRunnerAuthHeader ? { VALIDATE_RUNNER_AUTH: opts.validateRunnerAuthHeader } : {},
308729
+ // The suite receives a random run-scoped loopback callback, never the
308730
+ // global runner bearer credential.
308731
+ ...callbackProxy ? {
308732
+ VALIDATE_RUN_ID: runId,
308733
+ ...callbackClaimEpoch !== void 0 ? { VALIDATE_CLAIM_EPOCH: String(callbackClaimEpoch) } : {},
308734
+ VALIDATE_SERVER_URL: callbackProxy.serverUrl,
308735
+ VALIDATE_RUNNER_AUTH: callbackProxy.authHeader
308736
+ } : {},
308539
308737
  // Owning Fargate taskArn (per-task constant; unset off-Fargate). Forwarded
308540
308738
  // so persistCredential() can prove ownership to the /credential fence.
308541
- ...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {}
308739
+ ...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {},
308740
+ ...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
308542
308741
  };
308543
308742
  logs2 += `Running playwright --project=${opts.suiteProject ?? "suite"}...
308544
308743
  `;
@@ -308567,6 +308766,8 @@ var require_regression_runner = __commonJS({
308567
308766
  error: String(err)
308568
308767
  };
308569
308768
  } finally {
308769
+ await callbackProxy?.close().catch(() => {
308770
+ });
308570
308771
  (0, promises_1.rm)(extractDir, { recursive: true, force: true }).catch(() => {
308571
308772
  });
308572
308773
  await (0, browser_config_js_1.cleanupPlaywrightTempArtifacts)().catch(() => {
@@ -324810,6 +325011,455 @@ var require_resource_governor = __commonJS({
324810
325011
  }
324811
325012
  });
324812
325013
 
325014
+ // ../runner-core/dist/services/local-auth-state.js
325015
+ var require_local_auth_state = __commonJS({
325016
+ "../runner-core/dist/services/local-auth-state.js"(exports) {
325017
+ "use strict";
325018
+ Object.defineProperty(exports, "__esModule", { value: true });
325019
+ exports.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS = exports.LOCAL_AUTH_STATE_LOCK_STALE_MS = exports.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS = exports.LOCAL_AUTH_STATE_WAIT_MS = exports.LOCAL_AUTH_STATE_MAX_AGE_MS = void 0;
325020
+ exports.shouldSkipWarmAuthSetup = shouldSkipWarmAuthSetup2;
325021
+ exports.shouldSkipDisabledAutomaticAuthSetup = shouldSkipDisabledAutomaticAuthSetup2;
325022
+ exports.localAuthStateFingerprint = localAuthStateFingerprint;
325023
+ exports.ensurePrivateDirectory = ensurePrivateDirectory2;
325024
+ exports.isValidStorageState = isValidStorageState;
325025
+ exports.isValidStorageStateJson = isValidStorageStateJson;
325026
+ exports.isValidLocalAuthStateFile = isValidLocalAuthStateFile;
325027
+ exports.cleanupLocalAuthStateFiles = cleanupLocalAuthStateFiles;
325028
+ exports.cleanupExpiredLocalAuthStateFiles = cleanupExpiredLocalAuthStateFiles2;
325029
+ exports.localAuthStatePaths = localAuthStatePaths2;
325030
+ exports.getValidLocalAuthState = getValidLocalAuthState2;
325031
+ exports.waitForValidLocalAuthState = waitForValidLocalAuthState;
325032
+ exports.waitForLocalAuthStateProducer = waitForLocalAuthStateProducer2;
325033
+ exports.promoteLocalAuthStateCapture = promoteLocalAuthStateCapture2;
325034
+ exports.discardLocalAuthStateCapture = discardLocalAuthStateCapture2;
325035
+ exports.acquireLocalAuthStateLock = acquireLocalAuthStateLock2;
325036
+ exports.localAuthStateFilename = localAuthStateFilename;
325037
+ var node_fs_1 = __require("fs");
325038
+ var node_crypto_1 = __require("crypto");
325039
+ var node_path_1 = __require("path");
325040
+ var auth_recipe_js_1 = require_auth_recipe();
325041
+ exports.LOCAL_AUTH_STATE_MAX_AGE_MS = 60 * 60 * 1e3;
325042
+ exports.LOCAL_AUTH_STATE_WAIT_MS = 3 * 60 * 1e3;
325043
+ exports.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS = 15 * 60 * 1e3;
325044
+ exports.LOCAL_AUTH_STATE_LOCK_STALE_MS = 2 * 60 * 1e3;
325045
+ exports.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS = 30 * 60 * 1e3;
325046
+ function shouldSkipWarmAuthSetup2(authForceRun, sharedAuthEnabled, cacheWarm) {
325047
+ return sharedAuthEnabled && authForceRun !== true && cacheWarm;
325048
+ }
325049
+ function shouldSkipDisabledAutomaticAuthSetup2(authForceRun, authVerify, sharedAuthEnabled) {
325050
+ return !sharedAuthEnabled && authForceRun !== true && authVerify !== true;
325051
+ }
325052
+ function normalizeOrigin(value) {
325053
+ try {
325054
+ return new URL(value).origin.toLowerCase();
325055
+ } catch {
325056
+ return value.trim().toLowerCase();
325057
+ }
325058
+ }
325059
+ function normalizeAppUrl(value) {
325060
+ try {
325061
+ const url = new URL(value);
325062
+ url.hash = "";
325063
+ return `${url.origin.toLowerCase()}${url.pathname}${url.search}`;
325064
+ } catch {
325065
+ return value.trim();
325066
+ }
325067
+ }
325068
+ function safeSegment(value, fallback) {
325069
+ const safe = value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 120);
325070
+ return safe || fallback;
325071
+ }
325072
+ function stableVariables(testVariables) {
325073
+ return Object.entries(testVariables).filter(([key]) => !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(key)).sort(([a], [b]) => a.localeCompare(b));
325074
+ }
325075
+ function localAuthStateFingerprint(input) {
325076
+ const payload = JSON.stringify({
325077
+ version: 1,
325078
+ projectId: input.projectId,
325079
+ serverOrigin: normalizeOrigin(input.serverUrl),
325080
+ appUrl: normalizeAppUrl(input.baseUrl),
325081
+ testVariables: stableVariables(input.testVariables)
325082
+ });
325083
+ const key = input.authToken || `validate-local-auth:${input.projectId}`;
325084
+ return (0, node_crypto_1.createHmac)("sha256", key).update(payload).digest("hex").slice(0, 24);
325085
+ }
325086
+ function ensurePrivateDirectory2(dir) {
325087
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 448 });
325088
+ try {
325089
+ (0, node_fs_1.chmodSync)(dir, 448);
325090
+ } catch {
325091
+ }
325092
+ }
325093
+ function removeFile(path) {
325094
+ try {
325095
+ (0, node_fs_1.unlinkSync)(path);
325096
+ } catch {
325097
+ }
325098
+ }
325099
+ function readLockOwner(path) {
325100
+ try {
325101
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
325102
+ return typeof parsed.token === "string" && parsed.token.length > 0 && Number.isSafeInteger(parsed.pid) && (parsed.pid ?? 0) > 0 ? { token: parsed.token, pid: parsed.pid } : void 0;
325103
+ } catch {
325104
+ return void 0;
325105
+ }
325106
+ }
325107
+ function processIsAlive(pid) {
325108
+ try {
325109
+ process.kill(pid, 0);
325110
+ return true;
325111
+ } catch (error2) {
325112
+ return error2.code === "EPERM";
325113
+ }
325114
+ }
325115
+ function staleLockCanBeReaped(path, now, staleMs) {
325116
+ try {
325117
+ const ageMs = now - (0, node_fs_1.statSync)(path).mtimeMs;
325118
+ if (ageMs < staleMs)
325119
+ return false;
325120
+ const owner = readLockOwner(path);
325121
+ if (owner) {
325122
+ if (!processIsAlive(owner.pid))
325123
+ return true;
325124
+ return ageMs >= Math.max(staleMs * 5, exports.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS);
325125
+ }
325126
+ return ageMs >= staleMs * 5;
325127
+ } catch {
325128
+ return false;
325129
+ }
325130
+ }
325131
+ function isRecord(value) {
325132
+ return !!value && typeof value === "object" && !Array.isArray(value);
325133
+ }
325134
+ function isValidStorageState(value) {
325135
+ if (!isRecord(value))
325136
+ return false;
325137
+ const cookies = value.cookies;
325138
+ const origins = value.origins;
325139
+ if (!Array.isArray(cookies) || !Array.isArray(origins))
325140
+ return false;
325141
+ const cookiesValid = cookies.every((cookie) => {
325142
+ if (!isRecord(cookie))
325143
+ return false;
325144
+ return typeof cookie.name === "string" && typeof cookie.value === "string" && typeof cookie.domain === "string" && typeof cookie.path === "string" && typeof cookie.expires === "number" && Number.isFinite(cookie.expires) && typeof cookie.httpOnly === "boolean" && typeof cookie.secure === "boolean" && (cookie.sameSite === "Strict" || cookie.sameSite === "Lax" || cookie.sameSite === "None");
325145
+ });
325146
+ if (!cookiesValid)
325147
+ return false;
325148
+ const originsValid = origins.every((origin) => {
325149
+ if (!isRecord(origin) || typeof origin.origin !== "string" || !Array.isArray(origin.localStorage))
325150
+ return false;
325151
+ return origin.localStorage.every((entry) => isRecord(entry) && typeof entry.name === "string" && typeof entry.value === "string");
325152
+ });
325153
+ if (!originsValid)
325154
+ return false;
325155
+ return cookies.length > 0 || origins.some((origin) => origin.localStorage.length > 0);
325156
+ }
325157
+ function isValidStorageStateJson(value) {
325158
+ try {
325159
+ return isValidStorageState(JSON.parse(value));
325160
+ } catch {
325161
+ return false;
325162
+ }
325163
+ }
325164
+ function isStorageStateShape(value) {
325165
+ if (!value || typeof value !== "object")
325166
+ return false;
325167
+ return isValidStorageState(value);
325168
+ }
325169
+ function isValidLocalAuthStateFile(path) {
325170
+ try {
325171
+ return isStorageStateShape(JSON.parse((0, node_fs_1.readFileSync)(path, "utf8")));
325172
+ } catch {
325173
+ return false;
325174
+ }
325175
+ }
325176
+ function cleanupLocalAuthStateFiles(runsDir, projectId, now = Date.now(), maxAgeMs = exports.LOCAL_AUTH_STATE_MAX_AGE_MS) {
325177
+ ensurePrivateDirectory2(runsDir);
325178
+ const projectSegment = safeSegment(projectId, "default");
325179
+ const legacyName = `auth-state-${projectSegment}.json`;
325180
+ const prefix = `auth-state-${projectSegment}-`;
325181
+ for (const name of (0, node_fs_1.readdirSync)(runsDir)) {
325182
+ if (name === legacyName) {
325183
+ removeFile((0, node_path_1.join)(runsDir, name));
325184
+ continue;
325185
+ }
325186
+ if (!name.startsWith(prefix))
325187
+ continue;
325188
+ const path = (0, node_path_1.join)(runsDir, name);
325189
+ try {
325190
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs) {
325191
+ if (!name.endsWith(".lock") || staleLockCanBeReaped(path, now, maxAgeMs))
325192
+ removeFile(path);
325193
+ }
325194
+ } catch {
325195
+ }
325196
+ }
325197
+ }
325198
+ function cleanupExpiredLocalAuthStateFiles2(runsDir, now = Date.now(), maxAgeMs = exports.LOCAL_AUTH_STATE_MAX_AGE_MS) {
325199
+ ensurePrivateDirectory2(runsDir);
325200
+ for (const name of (0, node_fs_1.readdirSync)(runsDir)) {
325201
+ if (!name.startsWith("auth-state-"))
325202
+ continue;
325203
+ const path = (0, node_path_1.join)(runsDir, name);
325204
+ try {
325205
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs) {
325206
+ if (!name.endsWith(".lock") || staleLockCanBeReaped(path, now, maxAgeMs))
325207
+ removeFile(path);
325208
+ }
325209
+ } catch {
325210
+ }
325211
+ }
325212
+ }
325213
+ function localAuthStatePaths2(runsDir, input, runId) {
325214
+ cleanupLocalAuthStateFiles(runsDir, input.projectId);
325215
+ const projectSegment = safeSegment(input.projectId, "default");
325216
+ const runSegment = safeSegment(runId, `${process.pid}`);
325217
+ const fingerprint = localAuthStateFingerprint(input);
325218
+ const cachePath = (0, node_path_1.join)(runsDir, `auth-state-${projectSegment}-${fingerprint}.json`);
325219
+ return {
325220
+ cachePath,
325221
+ capturePath: `${cachePath}.capture-${runSegment}-${process.pid}.tmp`,
325222
+ fingerprint,
325223
+ lockKey: `${normalizeOrigin(input.serverUrl)}:${projectSegment}:${fingerprint}`,
325224
+ lockPath: `${cachePath}.lock`
325225
+ };
325226
+ }
325227
+ function getValidLocalAuthState2(path, now = Date.now(), maxAgeMs = exports.LOCAL_AUTH_STATE_MAX_AGE_MS) {
325228
+ if (!(0, node_fs_1.existsSync)(path))
325229
+ return void 0;
325230
+ try {
325231
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs || !isValidLocalAuthStateFile(path)) {
325232
+ removeFile(path);
325233
+ return void 0;
325234
+ }
325235
+ return path;
325236
+ } catch {
325237
+ return void 0;
325238
+ }
325239
+ }
325240
+ async function waitForValidLocalAuthState(path, timeoutMs = exports.LOCAL_AUTH_STATE_WAIT_MS, pollMs = 100) {
325241
+ const deadline = Date.now() + Math.max(0, timeoutMs);
325242
+ do {
325243
+ const valid = getValidLocalAuthState2(path);
325244
+ if (valid)
325245
+ return valid;
325246
+ const remaining = deadline - Date.now();
325247
+ if (remaining <= 0)
325248
+ return void 0;
325249
+ await new Promise((resolve2) => setTimeout(resolve2, Math.min(Math.max(10, pollMs), remaining)));
325250
+ } while (true);
325251
+ }
325252
+ async function waitForLocalAuthStateProducer2(path, lockKey, startupGraceMs = 2e3, maxWaitMs = exports.LOCAL_AUTH_STATE_WAIT_MS, pollMs = 100, lockPath) {
325253
+ const startedAt = Date.now();
325254
+ const producerIsActive = () => lockTails.has(lockKey) || !!lockPath && (0, node_fs_1.existsSync)(lockPath);
325255
+ let sawProducer = producerIsActive();
325256
+ do {
325257
+ const valid = getValidLocalAuthState2(path);
325258
+ if (valid)
325259
+ return valid;
325260
+ const producerActive = producerIsActive();
325261
+ if (producerActive)
325262
+ sawProducer = true;
325263
+ const elapsed = Date.now() - startedAt;
325264
+ if (sawProducer && !producerActive)
325265
+ return void 0;
325266
+ if (!sawProducer && elapsed >= Math.max(0, startupGraceMs))
325267
+ return void 0;
325268
+ if (elapsed >= Math.max(0, maxWaitMs))
325269
+ return void 0;
325270
+ await new Promise((resolve2) => setTimeout(resolve2, Math.max(10, pollMs)));
325271
+ } while (true);
325272
+ }
325273
+ function promoteLocalAuthStateCapture2(capturePath, cachePath, replaceExisting = false, lease) {
325274
+ if (lease && !lease.isOwner()) {
325275
+ removeFile(capturePath);
325276
+ return false;
325277
+ }
325278
+ if (!(0, node_fs_1.existsSync)(capturePath) || !isValidLocalAuthStateFile(capturePath)) {
325279
+ removeFile(capturePath);
325280
+ return false;
325281
+ }
325282
+ try {
325283
+ (0, node_fs_1.chmodSync)(capturePath, 384);
325284
+ } catch {
325285
+ }
325286
+ try {
325287
+ if (lease && !lease.isOwner()) {
325288
+ removeFile(capturePath);
325289
+ return false;
325290
+ }
325291
+ if ((0, node_fs_1.existsSync)(cachePath)) {
325292
+ if (!replaceExisting && isValidLocalAuthStateFile(cachePath)) {
325293
+ removeFile(capturePath);
325294
+ return true;
325295
+ }
325296
+ removeFile(cachePath);
325297
+ }
325298
+ if (lease && !lease.isOwner()) {
325299
+ removeFile(capturePath);
325300
+ return false;
325301
+ }
325302
+ (0, node_fs_1.renameSync)(capturePath, cachePath);
325303
+ try {
325304
+ (0, node_fs_1.chmodSync)(cachePath, 384);
325305
+ } catch {
325306
+ }
325307
+ return true;
325308
+ } catch {
325309
+ const winnerIsValid = (0, node_fs_1.existsSync)(cachePath) && isValidLocalAuthStateFile(cachePath);
325310
+ removeFile(capturePath);
325311
+ return winnerIsValid;
325312
+ }
325313
+ }
325314
+ function discardLocalAuthStateCapture2(capturePath) {
325315
+ removeFile(capturePath);
325316
+ }
325317
+ var lockTails = /* @__PURE__ */ new Map();
325318
+ function publishCompleteLockOwner(lockPath, owner, linkFile) {
325319
+ const metadata = JSON.stringify(owner);
325320
+ const tempPath = `${lockPath}.${owner.pid}.${owner.token}.tmp`;
325321
+ let createdLock = false;
325322
+ try {
325323
+ (0, node_fs_1.writeFileSync)(tempPath, metadata, { flag: "wx", mode: 384 });
325324
+ try {
325325
+ linkFile(tempPath, lockPath);
325326
+ createdLock = true;
325327
+ return (0, node_fs_1.openSync)(lockPath, "r+");
325328
+ } catch (error2) {
325329
+ const code = error2.code;
325330
+ if (!["EPERM", "EACCES", "EXDEV", "ENOSYS", "ENOTSUP", "EOPNOTSUPP"].includes(code ?? "")) {
325331
+ throw error2;
325332
+ }
325333
+ let fd;
325334
+ try {
325335
+ fd = (0, node_fs_1.openSync)(lockPath, "wx+", 384);
325336
+ createdLock = true;
325337
+ (0, node_fs_1.writeFileSync)(fd, metadata, { encoding: "utf8" });
325338
+ (0, node_fs_1.fsyncSync)(fd);
325339
+ return fd;
325340
+ } catch (fallbackError) {
325341
+ if (fd !== void 0) {
325342
+ try {
325343
+ (0, node_fs_1.closeSync)(fd);
325344
+ } catch {
325345
+ }
325346
+ }
325347
+ if (createdLock)
325348
+ removeFile(lockPath);
325349
+ throw fallbackError;
325350
+ }
325351
+ }
325352
+ } catch (error2) {
325353
+ if (createdLock && readLockOwner(lockPath)?.token === owner.token)
325354
+ removeFile(lockPath);
325355
+ throw error2;
325356
+ } finally {
325357
+ removeFile(tempPath);
325358
+ }
325359
+ }
325360
+ async function acquireExclusiveFileLock(lockPath, linkFile = node_fs_1.linkSync) {
325361
+ ensurePrivateDirectory2((0, node_path_1.dirname)(lockPath));
325362
+ const startedAt = Date.now();
325363
+ while (true) {
325364
+ try {
325365
+ const ownerToken = (0, node_crypto_1.randomBytes)(24).toString("base64url");
325366
+ const fd = publishCompleteLockOwner(lockPath, { token: ownerToken, pid: process.pid }, linkFile);
325367
+ const ownsPath = () => {
325368
+ try {
325369
+ const owner = readLockOwner(lockPath);
325370
+ return owner?.token === ownerToken && owner.pid === process.pid;
325371
+ } catch {
325372
+ return false;
325373
+ }
325374
+ };
325375
+ const heartbeat = setInterval(() => {
325376
+ try {
325377
+ if (!ownsPath()) {
325378
+ clearInterval(heartbeat);
325379
+ return;
325380
+ }
325381
+ const now = /* @__PURE__ */ new Date();
325382
+ (0, node_fs_1.futimesSync)(fd, now, now);
325383
+ } catch {
325384
+ clearInterval(heartbeat);
325385
+ }
325386
+ }, Math.max(1e3, Math.floor(exports.LOCAL_AUTH_STATE_LOCK_STALE_MS / 3)));
325387
+ heartbeat.unref();
325388
+ let released = false;
325389
+ const release = (() => {
325390
+ if (released)
325391
+ return;
325392
+ released = true;
325393
+ clearInterval(heartbeat);
325394
+ try {
325395
+ if (ownsPath())
325396
+ removeFile(lockPath);
325397
+ } catch {
325398
+ }
325399
+ try {
325400
+ (0, node_fs_1.closeSync)(fd);
325401
+ } catch {
325402
+ }
325403
+ });
325404
+ release.isOwner = () => !released && ownsPath();
325405
+ return release;
325406
+ } catch (error2) {
325407
+ const code = error2.code;
325408
+ if (code !== "EEXIST")
325409
+ throw error2;
325410
+ try {
325411
+ if (staleLockCanBeReaped(lockPath, Date.now(), exports.LOCAL_AUTH_STATE_LOCK_STALE_MS)) {
325412
+ removeFile(lockPath);
325413
+ continue;
325414
+ }
325415
+ } catch {
325416
+ continue;
325417
+ }
325418
+ if (Date.now() - startedAt >= exports.LOCAL_AUTH_STATE_WAIT_MS) {
325419
+ throw new Error(`Timed out waiting for auth-state lock: ${(0, node_path_1.basename)(lockPath)}`);
325420
+ }
325421
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
325422
+ }
325423
+ }
325424
+ }
325425
+ async function acquireLocalAuthStateLock2(key, lockPath, linkFile = node_fs_1.linkSync) {
325426
+ const previous = lockTails.get(key) ?? Promise.resolve();
325427
+ let unlockCurrent;
325428
+ const current = new Promise((resolve2) => {
325429
+ unlockCurrent = resolve2;
325430
+ });
325431
+ const tail = previous.then(() => current);
325432
+ lockTails.set(key, tail);
325433
+ await previous;
325434
+ let releaseFileLock;
325435
+ try {
325436
+ if (lockPath)
325437
+ releaseFileLock = await acquireExclusiveFileLock(lockPath, linkFile);
325438
+ } catch (error2) {
325439
+ unlockCurrent();
325440
+ if (lockTails.get(key) === tail)
325441
+ lockTails.delete(key);
325442
+ throw error2;
325443
+ }
325444
+ let released = false;
325445
+ const release = (() => {
325446
+ if (released)
325447
+ return;
325448
+ released = true;
325449
+ releaseFileLock?.();
325450
+ unlockCurrent();
325451
+ if (lockTails.get(key) === tail)
325452
+ lockTails.delete(key);
325453
+ });
325454
+ release.isOwner = () => !released && (releaseFileLock?.isOwner() ?? true);
325455
+ return release;
325456
+ }
325457
+ function localAuthStateFilename(path) {
325458
+ return (0, node_path_1.basename)(path);
325459
+ }
325460
+ }
325461
+ });
325462
+
324813
325463
  // ../../node_modules/adm-zip/util/constants.js
324814
325464
  var require_constants5 = __commonJS({
324815
325465
  "../../node_modules/adm-zip/util/constants.js"(exports, module) {
@@ -325123,7 +325773,7 @@ var require_utils2 = __commonJS({
325123
325773
  module.exports = Utils;
325124
325774
  Utils.prototype.makeDir = function(folder) {
325125
325775
  const self = this;
325126
- function mkdirSync5(fpath) {
325776
+ function mkdirSync4(fpath) {
325127
325777
  let resolvedPath = fpath.split(self.sep)[0];
325128
325778
  fpath.split(self.sep).forEach(function(name) {
325129
325779
  if (!name || name.substr(-1, 1) === ":") return;
@@ -325141,7 +325791,7 @@ var require_utils2 = __commonJS({
325141
325791
  if (stat && stat.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
325142
325792
  });
325143
325793
  }
325144
- mkdirSync5(folder);
325794
+ mkdirSync4(folder);
325145
325795
  };
325146
325796
  Utils.prototype.writeFileTo = function(path, content, overwrite, attr) {
325147
325797
  const self = this;
@@ -337065,6 +337715,8 @@ var require_dist2 = __commonJS({
337065
337715
  __exportStar(require_discover_agent(), exports);
337066
337716
  __exportStar(require_browser_pool(), exports);
337067
337717
  __exportStar(require_resource_governor(), exports);
337718
+ __exportStar(require_local_auth_state(), exports);
337719
+ __exportStar(require_run_callback_proxy(), exports);
337068
337720
  __exportStar(require_discover_explorer(), exports);
337069
337721
  __exportStar(require_discover_feature_explorer(), exports);
337070
337722
  __exportStar(require_discover_planner(), exports);
@@ -337341,7 +337993,7 @@ var require_package4 = __commonJS({
337341
337993
  "package.json"(exports, module) {
337342
337994
  module.exports = {
337343
337995
  name: "@validate.qa/runner",
337344
- version: "1.0.42",
337996
+ version: "1.0.43",
337345
337997
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
337346
337998
  bin: {
337347
337999
  "validate-runner": "dist/cli.js"
@@ -337349,7 +338001,7 @@ var require_package4 = __commonJS({
337349
338001
  scripts: {
337350
338002
  build: "tsup",
337351
338003
  dev: "tsx src/cli.ts",
337352
- test: "node --import tsx --test src/version-check.test.ts",
338004
+ test: "node --import tsx --test src/version-check.test.ts src/services/auth-state.test.ts",
337353
338005
  "test:mobile": "node --import tsx --test --test-concurrency=2 src/runner.mobile-ai-env.test.ts src/mobile-device-queue.test.ts src/device-lease.test.ts src/result-payload-budget.test.ts src/services/appium-*.test.ts src/services/*mobile*.test.ts src/services/mobile/*.test.ts",
337354
338006
  prepublishOnly: "npm run build"
337355
338007
  },
@@ -337428,7 +338080,6 @@ function getConfigDir() {
337428
338080
  }
337429
338081
 
337430
338082
  // src/runner.ts
337431
- import { existsSync as existsSync3, statSync, mkdirSync as mkdirSync4 } from "fs";
337432
338083
  import { join as join6 } from "path";
337433
338084
  import { homedir as homedir3 } from "os";
337434
338085
  import { createHash } from "crypto";
@@ -337456,23 +338107,33 @@ var import_runner_core6 = __toESM(require_dist2());
337456
338107
 
337457
338108
  // src/runner.ts
337458
338109
  var import_runner_core11 = __toESM(require_dist2());
338110
+ var import_runner_core12 = __toESM(require_dist2());
337459
338111
 
337460
338112
  // src/services/auth-state.ts
338113
+ init_dist();
337461
338114
  var INLINE_LOGIN_TAG = "@inline-login";
337462
338115
  var GUARDED_LOGIN_TAG = "@auth-guarded";
337463
338116
  function sharedAuthInjectEnabled() {
337464
- return process.env.SHARED_AUTH_INJECT === "true";
338117
+ return isSharedAuthInjectEnabled();
338118
+ }
338119
+ function hasGeneratedAuthGuard(playwrightCode) {
338120
+ if (!playwrightCode) return false;
338121
+ return /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
338122
+ }
338123
+ function shouldCaptureAuthState(tags, playwrightCode) {
338124
+ return sharedAuthInjectEnabled() && Array.isArray(tags) && tags.includes("@needs-auth") && tags.includes(GUARDED_LOGIN_TAG) && !tags.includes("@auth") && hasGeneratedAuthGuard(playwrightCode);
337465
338125
  }
337466
338126
  function hasInlineLoginSteps(playwrightCode) {
337467
338127
  if (!playwrightCode) return false;
337468
338128
  return /process\.env\.TEST_USER_EMAIL!?/.test(playwrightCode) && /process\.env\.TEST_USER_PASSWORD!?/.test(playwrightCode);
337469
338129
  }
337470
338130
  function shouldUseAuthState(tags, playwrightCode) {
338131
+ if (!sharedAuthInjectEnabled()) return false;
337471
338132
  if (!Array.isArray(tags)) return false;
337472
338133
  if (tags.includes("@auth")) return false;
337473
338134
  if (tags.includes("@api")) return true;
337474
338135
  if (!tags.includes("@needs-auth")) return false;
337475
- if (sharedAuthInjectEnabled() && tags.includes(GUARDED_LOGIN_TAG)) return true;
338136
+ if (tags.includes(GUARDED_LOGIN_TAG) && hasGeneratedAuthGuard(playwrightCode)) return true;
337476
338137
  const isGeneratedVariant = tags.includes("@edge") || tags.includes("@bug");
337477
338138
  const hasLegacyInlineLogin = tags.includes("@discovered") && !isGeneratedVariant && hasInlineLoginSteps(playwrightCode);
337478
338139
  return !tags.includes(INLINE_LOGIN_TAG) && !hasLegacyInlineLogin;
@@ -344185,8 +344846,8 @@ function withMediaReferences(result, refs) {
344185
344846
 
344186
344847
  // src/runner.ts
344187
344848
  init_dist();
344188
- var import_runner_core12 = __toESM(require_dist2());
344189
344849
  var import_runner_core13 = __toESM(require_dist2());
344850
+ var import_runner_core14 = __toESM(require_dist2());
344190
344851
  function fingerprintTest(test) {
344191
344852
  const code = test.framework === "APPIUM" ? [test.appiumCode ?? "", JSON.stringify(test.steps ?? [])].join("\n--steps--\n") : test.playwrightCode ?? "";
344192
344853
  return createHash("sha256").update(test.name).update("\n--suite--\n").update(test.suiteName ?? "").update("\n--type--\n").update(test.testType ?? "").update("\n--framework--\n").update(test.framework ?? "").update("\n--code--\n").update(code).digest("hex").slice(0, 24);
@@ -344310,21 +344971,11 @@ var log = {
344310
344971
  warn: (msg) => console.log(`${C2.yellow}\u26A0${C2.reset} ${msg}`),
344311
344972
  dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
344312
344973
  };
344313
- var AUTH_MAX_AGE_MS = 60 * 60 * 1e3;
344314
344974
  function getRunsDir() {
344315
344975
  const dir = process.env.RUNS_DIR || join6(homedir3(), ".validate.qa", "runs");
344316
- if (!existsSync3(dir)) mkdirSync4(dir, { recursive: true });
344976
+ (0, import_runner_core12.ensurePrivateDirectory)(dir);
344317
344977
  return dir;
344318
344978
  }
344319
- function authStatePath(projectId) {
344320
- return join6(getRunsDir(), `auth-state-${projectId || "default"}.json`);
344321
- }
344322
- function getValidAuthState(projectId) {
344323
- const path = authStatePath(projectId);
344324
- if (!existsSync3(path)) return void 0;
344325
- const age = Date.now() - statSync(path).mtimeMs;
344326
- return age < AUTH_MAX_AGE_MS ? path : void 0;
344327
- }
344328
344979
  function buildHeaders(authToken, projectId) {
344329
344980
  return {
344330
344981
  "Content-Type": "application/json",
@@ -344373,7 +345024,13 @@ async function claimRun(serverUrl, headers, runId) {
344373
345024
  if (res.status === 429) return false;
344374
345025
  if (!res.ok) throw new Error(`Claim failed: ${res.status}`);
344375
345026
  const body = await res.json();
344376
- return body.claimed;
345027
+ if (!body.claimed) {
345028
+ throw new Error("Claim response did not confirm ownership");
345029
+ }
345030
+ if (typeof body.claimEpoch !== "number" || !Number.isSafeInteger(body.claimEpoch) || body.claimEpoch < 0) {
345031
+ throw new Error("Claim response did not include a valid ownership epoch");
345032
+ }
345033
+ return body.claimEpoch;
344377
345034
  }
344378
345035
  async function postResult(serverUrl, headers, runId, result) {
344379
345036
  const MAX_ATTEMPTS = 3;
@@ -344541,12 +345198,12 @@ async function postCaptureResult(serverUrl, headers, runId, result) {
344541
345198
  });
344542
345199
  if (!res.ok) throw new Error(`Failed to post capture result: ${res.status}`);
344543
345200
  }
344544
- async function postCredentialRotation(serverUrl, headers, runId, body) {
345201
+ async function postCredentialRotation(serverUrl, headers, runId, body, claimEpoch) {
344545
345202
  try {
344546
345203
  const res = await fetch(`${serverUrl}/api/runner/runs/${runId}/credential`, {
344547
345204
  method: "POST",
344548
345205
  headers,
344549
- body: JSON.stringify(body),
345206
+ body: JSON.stringify({ ...body, ...claimEpoch !== void 0 ? { claimEpoch } : {} }),
344550
345207
  signal: AbortSignal.timeout(3e4)
344551
345208
  });
344552
345209
  if (!res.ok) {
@@ -344883,6 +345540,12 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
344883
345540
  liveBrowserHandle,
344884
345541
  onLiveBrowserSnapshot: requestLiveBrowserHeartbeat
344885
345542
  });
345543
+ const callbackContext = {
345544
+ runId: run2.runId,
345545
+ serverUrl: opts.serverUrl,
345546
+ authHeader: headers.Authorization,
345547
+ claimEpoch: run2.claimEpoch
345548
+ };
344886
345549
  const startTime = Date.now();
344887
345550
  try {
344888
345551
  if (mobileLeaseError) {
@@ -344912,7 +345575,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
344912
345575
  log.fail(`PRE-FLIGHT FAILED - ${preflightError}`);
344913
345576
  return;
344914
345577
  }
344915
- const preflight = await (0, import_runner_core12.resolveBaseUrlPreflight)(run2.baseUrl);
345578
+ const preflight = await (0, import_runner_core13.resolveBaseUrlPreflight)(run2.baseUrl);
344916
345579
  if (preflight.error) {
344917
345580
  await postResult(opts.serverUrl, headers, run2.runId, { status: "ERROR", error: preflight.error });
344918
345581
  log.fail(`PRE-FLIGHT FAILED - ${preflight.error}`);
@@ -344929,31 +345592,72 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
344929
345592
  log.fail("ERROR - No auth setup code");
344930
345593
  return;
344931
345594
  }
344932
- const runAuthPath = authStatePath(run2.projectId || opts.projectId || "default");
344933
- log.info(`Auth setup: saving session state to ${runAuthPath}`);
344934
- const result2 = await runNativeWithLiveBrowser({
344935
- playwrightCode: run2.playwrightCode,
345595
+ const sharedAuthEnabled = sharedAuthInjectEnabled();
345596
+ if ((0, import_runner_core12.shouldSkipDisabledAutomaticAuthSetup)(run2.authForceRun, run2.authVerify, sharedAuthEnabled)) {
345597
+ const duration2 = Date.now() - startTime;
345598
+ await postResult(opts.serverUrl, headers, run2.runId, {
345599
+ status: "PASSED",
345600
+ logs: "Automatic auth prewarm skipped: shared session reuse is disabled.",
345601
+ duration: duration2
345602
+ });
345603
+ log.info(`Auth prewarm skipped (${duration2}ms) - SHARED_AUTH_INJECT=false`);
345604
+ return;
345605
+ }
345606
+ const authPaths2 = sharedAuthEnabled ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
345607
+ projectId: run2.projectId || opts.projectId || "default",
345608
+ serverUrl: opts.serverUrl,
344936
345609
  baseUrl: run2.baseUrl,
344937
- testName: "auth-setup",
344938
- testVariables: { ...run2.testVariables, AUTH_FILE: runAuthPath },
344939
- validateContext: {
344940
- runId: run2.runId,
344941
- serverUrl: opts.serverUrl,
344942
- authHeader: headers.Authorization
345610
+ testVariables: run2.testVariables,
345611
+ authToken: opts.authToken
345612
+ }, run2.runId) : void 0;
345613
+ const releaseAuthLock2 = authPaths2 ? await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths2.lockKey, authPaths2.lockPath) : void 0;
345614
+ try {
345615
+ if (authPaths2 && (0, import_runner_core12.shouldSkipWarmAuthSetup)(
345616
+ run2.authForceRun,
345617
+ sharedAuthEnabled,
345618
+ !!(0, import_runner_core12.getValidLocalAuthState)(authPaths2.cachePath)
345619
+ )) {
345620
+ const duration3 = Date.now() - startTime;
345621
+ await postResult(opts.serverUrl, headers, run2.runId, {
345622
+ status: "PASSED",
345623
+ logs: "Auth setup skipped: the credential-bound local session is already warm.",
345624
+ duration: duration3
345625
+ });
345626
+ log.ok(`Auth setup skipped (${duration3}ms) - local session already warm`);
345627
+ return;
344943
345628
  }
344944
- });
344945
- const duration2 = Date.now() - startTime;
344946
- await postResult(opts.serverUrl, headers, run2.runId, {
344947
- status: result2.passed ? "PASSED" : "FAILED",
344948
- apiCalls: result2.apiCalls,
344949
- logs: result2.logs,
344950
- error: result2.error,
344951
- duration: duration2
344952
- });
344953
- log[result2.passed ? "ok" : "fail"](
344954
- `Auth setup ${result2.passed ? "PASSED" : "FAILED"} (${duration2}ms)${result2.error ? ` - ${result2.error}` : ""}`
344955
- );
344956
- return;
345629
+ log.info(run2.authVerify ? "Auth verification: replaying the current login recipe" : sharedAuthEnabled ? "Auth setup: capturing a credential-bound local session" : "Auth setup: running login with shared session reuse disabled");
345630
+ const capturePath = sharedAuthEnabled && !run2.authVerify ? authPaths2?.capturePath : void 0;
345631
+ const result2 = await runNativeWithLiveBrowser({
345632
+ playwrightCode: run2.playwrightCode,
345633
+ baseUrl: run2.baseUrl,
345634
+ testName: "auth-setup",
345635
+ testVariables: run2.testVariables,
345636
+ authStateCapturePath: capturePath,
345637
+ validateContext: callbackContext
345638
+ });
345639
+ const captured = !!capturePath && !!authPaths2 && result2.passed && !result2.suspiciousPass && (0, import_runner_core12.promoteLocalAuthStateCapture)(
345640
+ capturePath,
345641
+ authPaths2.cachePath,
345642
+ run2.authForceRun === true,
345643
+ releaseAuthLock2
345644
+ );
345645
+ const duration2 = Date.now() - startTime;
345646
+ await postResult(opts.serverUrl, headers, run2.runId, {
345647
+ status: result2.passed ? "PASSED" : "FAILED",
345648
+ apiCalls: result2.apiCalls,
345649
+ logs: result2.logs,
345650
+ error: result2.error,
345651
+ duration: duration2
345652
+ });
345653
+ log[result2.passed ? "ok" : "fail"](
345654
+ `Auth setup ${result2.passed ? "PASSED" : "FAILED"} (${duration2}ms)${captured ? " - session cached" : ""}${result2.error ? ` - ${result2.error}` : ""}`
345655
+ );
345656
+ return;
345657
+ } finally {
345658
+ if (authPaths2?.capturePath) (0, import_runner_core12.discardLocalAuthStateCapture)(authPaths2.capturePath);
345659
+ releaseAuthLock2?.();
345660
+ }
344957
345661
  }
344958
345662
  if (runMode === "batch-heal") {
344959
345663
  const tests = run2.batchHealTests ?? [];
@@ -344972,7 +345676,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
344972
345676
  const batchResult = await (0, import_runner_core10.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
344973
345677
  testVariables: run2.testVariables,
344974
345678
  runNativeTest: runNativeWithLiveBrowser,
344975
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
345679
+ validateContext: callbackContext,
344976
345680
  onLog: healLogStream.onLog
344977
345681
  });
344978
345682
  await healLogStream.flushNow();
@@ -345008,6 +345712,15 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
345008
345712
  );
345009
345713
  const describeKind = isSerialDescribe ? "serial" : "non-serial";
345010
345714
  log.info(`API test heal - ${subTestCount} sub-test(s) [${describeKind}], ${failingSubTestCount} budgeted, ${MAX_API_HEAL_ATTEMPTS} attempts`);
345715
+ const apiHealAuthPaths = shouldUseAuthState(run2.tags, run2.playwrightCode) ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
345716
+ projectId: run2.projectId || opts.projectId,
345717
+ serverUrl: opts.serverUrl,
345718
+ baseUrl: run2.baseUrl,
345719
+ testVariables: run2.testVariables,
345720
+ authToken: opts.authToken
345721
+ }, run2.runId) : void 0;
345722
+ const apiHealStorageStatePath = apiHealAuthPaths ? (0, import_runner_core12.getValidLocalAuthState)(apiHealAuthPaths.cachePath) : void 0;
345723
+ if (apiHealStorageStatePath) log.info("API heal verification is using saved auth state");
345011
345724
  const stickyAppBugs = /* @__PURE__ */ new Map();
345012
345725
  const lastChangeByTitle = /* @__PURE__ */ new Map();
345013
345726
  let currentError = run2.lastFailureError ?? "Test failed";
@@ -345073,7 +345786,8 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
345073
345786
  playwrightCode: lastHealedCode,
345074
345787
  baseUrl: run2.baseUrl,
345075
345788
  testVariables: run2.testVariables,
345076
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization }
345789
+ storageStatePath: apiHealStorageStatePath,
345790
+ validateContext: callbackContext
345077
345791
  });
345078
345792
  if (finalVerifyResult.passed) {
345079
345793
  log.ok(` Attempt ${attempt} terminal verification PASSED (${bugCount} app_bug, ${passCount} pass)`);
@@ -345093,7 +345807,9 @@ ${finalVerifyResult.logs ?? ""}`;
345093
345807
  finalVerifyResult = await runNativeWithLiveBrowser({
345094
345808
  playwrightCode: lastHealedCode,
345095
345809
  baseUrl: run2.baseUrl,
345096
- testVariables: run2.testVariables
345810
+ testVariables: run2.testVariables,
345811
+ storageStatePath: apiHealStorageStatePath,
345812
+ validateContext: callbackContext
345097
345813
  });
345098
345814
  const stepResults = finalVerifyResult.stepResults ?? [];
345099
345815
  const nonAppBugFails = stepResults.filter(
@@ -345143,7 +345859,8 @@ ${finalVerifyResult.logs ?? ""}`;
345143
345859
  playwrightCode: lastHealedCode,
345144
345860
  baseUrl: run2.baseUrl,
345145
345861
  testVariables: run2.testVariables,
345146
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization }
345862
+ storageStatePath: apiHealStorageStatePath,
345863
+ validateContext: callbackContext
345147
345864
  });
345148
345865
  if (finalVerifyResult.passed) {
345149
345866
  log.ok(` Attempt ${attempt} verification PASSED`);
@@ -345196,7 +345913,7 @@ ${finalVerifyResult.logs ?? ""}`;
345196
345913
  if (classifyErrorMessage(composedError ?? null) === "config_issue") {
345197
345914
  apiHealStatus = "ERROR";
345198
345915
  } else {
345199
- const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
345916
+ const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
345200
345917
  testName: run2.testName ?? run2.testCaseId ?? void 0,
345201
345918
  tags: run2.tags ?? [],
345202
345919
  error: composedError ?? finalVerifyResult?.error ?? null,
@@ -345293,8 +346010,8 @@ ${finalVerifyResult.logs ?? ""}`;
345293
346010
  lastHealAttempt: run2.lastHealAttempt ?? null,
345294
346011
  lastHealedCode: run2.lastHealedCode ?? null,
345295
346012
  failureScreenshot: run2.failureScreenshot ?? null,
345296
- onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }),
345297
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
346013
+ onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }, run2.claimEpoch),
346014
+ validateContext: callbackContext,
345298
346015
  liveBrowserHandle,
345299
346016
  runNativeTest: runNativeWithLiveBrowser
345300
346017
  };
@@ -345336,7 +346053,7 @@ ${finalVerifyResult.logs ?? ""}`;
345336
346053
  let healStatus = result2.passed ? "PASSED" : isConfigIssue ? "ERROR" : "FAILED";
345337
346054
  let healError = result2.error;
345338
346055
  if (!result2.passed && !isConfigIssue) {
345339
- const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
346056
+ const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
345340
346057
  testName: run2.testName ?? run2.testCaseId ?? void 0,
345341
346058
  tags: run2.tags ?? [],
345342
346059
  error: result2.error,
@@ -345407,7 +346124,8 @@ ${finalVerifyResult.logs ?? ""}`;
345407
346124
  // Allow generated `persistCredential(...)` calls inside the suite to
345408
346125
  // POST back to the server with the runner's bearer token.
345409
346126
  validateServerUrl: opts.serverUrl,
345410
- validateRunnerAuthHeader: headers.Authorization
346127
+ validateRunnerAuthHeader: headers.Authorization,
346128
+ claimEpoch: run2.claimEpoch
345411
346129
  });
345412
346130
  const overall = result2.error && result2.testResults.length === 0 ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
345413
346131
  const rr = await fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/regression-result`, {
@@ -345462,7 +346180,7 @@ ${finalVerifyResult.logs ?? ""}`;
345462
346180
  playwrightCode: healedCode,
345463
346181
  baseUrl: run2.baseUrl,
345464
346182
  testVariables: run2.testVariables,
345465
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization }
346183
+ validateContext: callbackContext
345466
346184
  });
345467
346185
  results.push({
345468
346186
  testCaseId: failedTest.testCaseId,
@@ -345525,8 +346243,8 @@ ${finalVerifyResult.logs ?? ""}`;
345525
346243
  onAICall: healAudit,
345526
346244
  modelOverride: run2.healerModel,
345527
346245
  consecutiveFails: failedTest.consecutiveFails,
345528
- onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }),
345529
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
346246
+ onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }, run2.claimEpoch),
346247
+ validateContext: callbackContext,
345530
346248
  liveBrowserHandle,
345531
346249
  runNativeTest: runNativeWithLiveBrowser
345532
346250
  };
@@ -345731,6 +346449,13 @@ ${finalVerifyResult.logs ?? ""}`;
345731
346449
  }
345732
346450
  return;
345733
346451
  }
346452
+ ctx.onCredentialUpdate = async (input) => postCredentialRotation(
346453
+ opts.serverUrl,
346454
+ headers,
346455
+ run2.runId,
346456
+ { ...input, actor: "ai-explorer" },
346457
+ run2.claimEpoch
346458
+ );
345734
346459
  log.info(`Discovery (${ctx.mode}) on ${ctx.baseUrl}`);
345735
346460
  const { push: pushAudit, drain: drainAudit } = createAuditFlusher(
345736
346461
  `${opts.serverUrl}/api/runner/runs/${run2.runId}/ai-audit-batch`,
@@ -346433,7 +347158,7 @@ ${finalVerifyResult.logs ?? ""}`;
346433
347158
  featureDescription: ctx.featureDescription,
346434
347159
  featurePages: ctx.featurePages,
346435
347160
  appBrief: ctx.appBrief,
346436
- onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-capture" })
347161
+ onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-capture" }, run2.claimEpoch)
346437
347162
  });
346438
347163
  if (logBuffer.length > 0) {
346439
347164
  postCaptureLog(opts.serverUrl, headers, run2.runId, ctx.jobId, truncateTextTail(logBuffer.join("\n"), DISCOVERY_LOG_POST_MAX_CHARS, "capture log"));
@@ -346678,7 +347403,7 @@ ${finalVerifyResult.logs ?? ""}`;
346678
347403
  }
346679
347404
  return verify;
346680
347405
  };
346681
- return await (0, import_runner_core13.executeMobileLiveHeal)({
347406
+ return await (0, import_runner_core14.executeMobileLiveHeal)({
346682
347407
  steps: a.steps,
346683
347408
  target: a.target,
346684
347409
  testVariables: a.testVariables,
@@ -346695,7 +347420,7 @@ ${finalVerifyResult.logs ?? ""}`;
346695
347420
  }
346696
347421
  };
346697
347422
  log.info("\u{1FA7A} Mobile AI heal \u2014 Phase 1 (script) \u2192 Phase 2 (live device, lazy)...");
346698
- const heal = await (0, import_runner_core13.executeMobileTestWithHealing)(
347423
+ const heal = await (0, import_runner_core14.executeMobileTestWithHealing)(
346699
347424
  mobileSteps,
346700
347425
  healTarget,
346701
347426
  {
@@ -346708,7 +347433,7 @@ ${finalVerifyResult.logs ?? ""}`;
346708
347433
  healerOptions
346709
347434
  );
346710
347435
  const finalRes = nativeVerify.last;
346711
- const outcome = (0, import_runner_core13.computeMobileHealOutcome)({
347436
+ const outcome = (0, import_runner_core14.computeMobileHealOutcome)({
346712
347437
  heal,
346713
347438
  finalError: finalRes?.error ?? null,
346714
347439
  reproduceError,
@@ -346854,26 +347579,77 @@ ${result2.logs}`);
346854
347579
  }
346855
347580
  return;
346856
347581
  }
346857
- const storageStatePath = shouldUseAuthState(run2.tags, run2.playwrightCode) ? getValidAuthState(run2.projectId || opts.projectId) : void 0;
346858
- if (storageStatePath) {
346859
- log.info(`Using auth state for project ${run2.projectId || opts.projectId}`);
346860
- }
346861
- const result = await runNativeWithLiveBrowser({
346862
- playwrightCode: run2.playwrightCode,
347582
+ const projectId = run2.projectId || opts.projectId;
347583
+ const authStateExpected = shouldUseAuthState(run2.tags, run2.playwrightCode);
347584
+ const captureAuth = shouldCaptureAuthState(run2.tags, run2.playwrightCode);
347585
+ const authPaths = authStateExpected || captureAuth ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
347586
+ projectId,
347587
+ serverUrl: opts.serverUrl,
346863
347588
  baseUrl: run2.baseUrl,
346864
347589
  testVariables: run2.testVariables,
346865
- storageStatePath,
346866
- validateContext: {
346867
- runId: run2.runId,
346868
- serverUrl: opts.serverUrl,
346869
- authHeader: headers.Authorization
347590
+ authToken: opts.authToken
347591
+ }, run2.runId) : void 0;
347592
+ let releaseAuthLock;
347593
+ let storageStatePath;
347594
+ if (authPaths && authStateExpected) {
347595
+ releaseAuthLock = await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths.lockKey, authPaths.lockPath);
347596
+ storageStatePath = (0, import_runner_core12.getValidLocalAuthState)(authPaths.cachePath);
347597
+ if (storageStatePath || !captureAuth) {
347598
+ releaseAuthLock();
347599
+ releaseAuthLock = void 0;
347600
+ }
347601
+ if (!storageStatePath && !captureAuth && sharedAuthInjectEnabled()) {
347602
+ log.info(`Waiting for the auth producer to warm project ${projectId}`);
347603
+ storageStatePath = await (0, import_runner_core12.waitForLocalAuthStateProducer)(
347604
+ authPaths.cachePath,
347605
+ authPaths.lockKey,
347606
+ 2e3,
347607
+ void 0,
347608
+ 100,
347609
+ authPaths.lockPath
347610
+ );
346870
347611
  }
346871
- });
347612
+ }
347613
+ if (storageStatePath) {
347614
+ log.info(`Using auth state for project ${projectId}`);
347615
+ }
347616
+ const runAuthPath = captureAuth ? authPaths?.capturePath : void 0;
347617
+ const result = await (async () => {
347618
+ try {
347619
+ return await runNativeWithLiveBrowser({
347620
+ playwrightCode: run2.playwrightCode,
347621
+ baseUrl: run2.baseUrl,
347622
+ testVariables: run2.testVariables,
347623
+ authStateCapturePath: runAuthPath,
347624
+ storageStatePath,
347625
+ validateContext: callbackContext
347626
+ });
347627
+ } finally {
347628
+ if (runAuthPath && authPaths) {
347629
+ if ((0, import_runner_core12.getValidLocalAuthState)(runAuthPath)) {
347630
+ const releasePromotionLock = releaseAuthLock ? void 0 : await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths.lockKey, authPaths.lockPath);
347631
+ try {
347632
+ const captured = (0, import_runner_core12.promoteLocalAuthStateCapture)(
347633
+ runAuthPath,
347634
+ authPaths.cachePath,
347635
+ !!storageStatePath,
347636
+ releaseAuthLock ?? releasePromotionLock
347637
+ );
347638
+ if (captured) log.info(`Captured auth state for project ${projectId}`);
347639
+ } finally {
347640
+ releasePromotionLock?.();
347641
+ }
347642
+ }
347643
+ (0, import_runner_core12.discardLocalAuthStateCapture)(runAuthPath);
347644
+ }
347645
+ releaseAuthLock?.();
347646
+ }
347647
+ })();
346872
347648
  const duration = Date.now() - startTime;
346873
347649
  let runStatus = result.passed ? "PASSED" : "FAILED";
346874
347650
  let runError = result.error;
346875
347651
  if (!result.passed) {
346876
- const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
347652
+ const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
346877
347653
  testName: run2.testName ?? run2.testCaseId ?? void 0,
346878
347654
  tags: run2.tags ?? [],
346879
347655
  error: result.error,
@@ -346934,6 +347710,12 @@ async function startRunner(opts) {
346934
347710
  process.env.AUTH_TOKEN = authToken;
346935
347711
  process.env.RUNNER_TOKEN = authToken;
346936
347712
  if (projectId) process.env.PROJECT_ID = projectId;
347713
+ (0, import_runner_core12.cleanupExpiredLocalAuthStateFiles)(getRunsDir());
347714
+ const authCleanupTimer = setInterval(
347715
+ () => (0, import_runner_core12.cleanupExpiredLocalAuthStateFiles)(getRunsDir()),
347716
+ import_runner_core12.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS
347717
+ );
347718
+ authCleanupTimer.unref?.();
346937
347719
  const headers = buildHeaders(authToken, projectId);
346938
347720
  console.log("");
346939
347721
  console.log(`${C2.bold}${C2.cyan}validate.qa Local Runner${C2.reset}`);
@@ -347042,7 +347824,7 @@ async function startRunner(opts) {
347042
347824
  capture: envInt("MAX_CAPTURE", DEFAULT_TYPE_LIMITS.capture),
347043
347825
  run: envInt("MAX_RUN", DEFAULT_TYPE_LIMITS.run)
347044
347826
  };
347045
- const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core12.createResourceGovernorOptions)({
347827
+ const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core13.createResourceGovernorOptions)({
347046
347828
  isCloud: IS_CLOUD,
347047
347829
  maxConcurrent: MAX_CONCURRENT,
347048
347830
  typeLimits: TYPE_LIMITS
@@ -347210,8 +347992,8 @@ async function startRunner(opts) {
347210
347992
  android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
347211
347993
  } : { web: true, ios: false, android: false };
347212
347994
  const bridge = enableMobile ? getBridgeState() : void 0;
347213
- const resourceStatus = (0, import_runner_core12.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
347214
- const processInventory = await (0, import_runner_core12.getBrowserProcessInventory)();
347995
+ const resourceStatus = (0, import_runner_core13.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
347996
+ const processInventory = await (0, import_runner_core13.getBrowserProcessInventory)();
347215
347997
  lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
347216
347998
  lastInventoryKinds.clear();
347217
347999
  for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
@@ -347241,7 +348023,7 @@ async function startRunner(opts) {
347241
348023
  resourceSnapshot: resourceStatus.snapshot,
347242
348024
  resourceGovernor: resourceStatus.governor,
347243
348025
  processInventory,
347244
- aiQueue: (0, import_runner_core12.getRunnerAIQueueStats)(),
348026
+ aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
347245
348027
  liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
347246
348028
  // Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
347247
348029
  // Cached & cheap — see getCachedDoctor above.
@@ -347352,7 +348134,7 @@ async function startRunner(opts) {
347352
348134
  let pidsWatchdogWarned = false;
347353
348135
  const pidsWatchdog = setInterval(() => {
347354
348136
  if (shuttingDown) return;
347355
- const snapshot = (0, import_runner_core12.getResourceSnapshot)();
348137
+ const snapshot = (0, import_runner_core13.getResourceSnapshot)();
347356
348138
  const pct = snapshot.pidsPct;
347357
348139
  if (pct == null || !Number.isFinite(pct)) return;
347358
348140
  if (pct >= PIDS_RECYCLE_PCT) {
@@ -347392,7 +348174,7 @@ async function startRunner(opts) {
347392
348174
  log.dim(` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
347393
348175
  continue;
347394
348176
  }
347395
- const admission = (0, import_runner_core12.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
348177
+ const admission = (0, import_runner_core13.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
347396
348178
  mode: runMode,
347397
348179
  activeRuns,
347398
348180
  activeByType: Object.fromEntries(activeByType),
@@ -347470,9 +348252,9 @@ async function startRunner(opts) {
347470
348252
  leasedDeviceId = freeDeviceId;
347471
348253
  leasedDeviceIds.set(freeDeviceId, run2.runId);
347472
348254
  }
347473
- let claimed = false;
348255
+ let claimEpoch = false;
347474
348256
  try {
347475
- claimed = await claimRun(serverUrl, headers, run2.runId);
348257
+ claimEpoch = await claimRun(serverUrl, headers, run2.runId);
347476
348258
  } catch (err) {
347477
348259
  if (leasedDeviceId) {
347478
348260
  leasedDeviceIds.delete(leasedDeviceId);
@@ -347480,7 +348262,7 @@ async function startRunner(opts) {
347480
348262
  }
347481
348263
  throw err;
347482
348264
  }
347483
- if (!claimed) {
348265
+ if (claimEpoch === false) {
347484
348266
  log.dim(` Skipped ${run2.runId} (already claimed)`);
347485
348267
  if (leasedDeviceId) {
347486
348268
  leasedDeviceIds.delete(leasedDeviceId);
@@ -347488,6 +348270,7 @@ async function startRunner(opts) {
347488
348270
  }
347489
348271
  continue;
347490
348272
  }
348273
+ run2.claimEpoch = claimEpoch;
347491
348274
  activeRuns++;
347492
348275
  incrementType(runMode);
347493
348276
  incrementProject(runProject);