@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.js CHANGED
@@ -1756,7 +1756,18 @@ var init_constants = __esm({
1756
1756
  "MINIMAX_API_KEY",
1757
1757
  "ANTHROPIC_API_KEY",
1758
1758
  "NVIDIA_API_KEY",
1759
- "DEEPSEEK_API_KEY"
1759
+ "DEEPSEEK_API_KEY",
1760
+ // Run-scoped callback capabilities. The parent runner injects these after
1761
+ // sanitization; customer variables must never impersonate the control plane.
1762
+ "AUTH_FILE",
1763
+ "SHARED_AUTH_INJECT",
1764
+ "VALIDATE_RUN_ID",
1765
+ "VALIDATE_CLAIM_EPOCH",
1766
+ "VALIDATE_AUTH_CONFIG_HASH",
1767
+ "VALIDATE_TASK_ARN",
1768
+ "VALIDATE_RUNNER_AUTH",
1769
+ "VALIDATE_SERVER_URL",
1770
+ "VALIDATE_LIVE_BROWSER_STATE_PATH"
1760
1771
  ]);
1761
1772
  SPAWNED_ENV_ALLOWLIST = /* @__PURE__ */ new Set([
1762
1773
  // Runtime essentials - Node.js and Playwright need these to function
@@ -5024,6 +5035,16 @@ var init_voice_config = __esm({
5024
5035
  }
5025
5036
  });
5026
5037
 
5038
+ // ../shared/dist/feature-flags.js
5039
+ function isSharedAuthInjectEnabled(value = process.env.SHARED_AUTH_INJECT) {
5040
+ return value?.trim().toLowerCase() !== "false";
5041
+ }
5042
+ var init_feature_flags = __esm({
5043
+ "../shared/dist/feature-flags.js"() {
5044
+ "use strict";
5045
+ }
5046
+ });
5047
+
5027
5048
  // ../shared/dist/index.js
5028
5049
  var dist_exports = {};
5029
5050
  __export(dist_exports, {
@@ -5147,6 +5168,7 @@ __export(dist_exports, {
5147
5168
  isRunnableStep: () => isRunnableStep,
5148
5169
  isSafeDeepLink: () => isSafeDeepLink,
5149
5170
  isSameRegistrableDomainHost: () => isSameRegistrableDomainHost,
5171
+ isSharedAuthInjectEnabled: () => isSharedAuthInjectEnabled,
5150
5172
  isSttModelForProvider: () => isSttModelForProvider,
5151
5173
  isTestReadyForPlatform: () => isTestReadyForPlatform,
5152
5174
  isTrialExpired: () => isTrialExpired,
@@ -5213,6 +5235,7 @@ var init_dist = __esm({
5213
5235
  init_test_counting();
5214
5236
  init_origin_domain();
5215
5237
  init_voice_config();
5238
+ init_feature_flags();
5216
5239
  }
5217
5240
  });
5218
5241
 
@@ -10676,7 +10699,7 @@ var require_transport = __commonJS({
10676
10699
  "../../node_modules/pino/lib/transport.js"(exports2, module2) {
10677
10700
  "use strict";
10678
10701
  var { createRequire } = require("module");
10679
- var { existsSync: existsSync4 } = require("fs");
10702
+ var { existsSync: existsSync3 } = require("fs");
10680
10703
  var getCallers = require_caller();
10681
10704
  var { join: join7, isAbsolute, sep } = require("path");
10682
10705
  var { fileURLToPath } = require("url");
@@ -10750,7 +10773,7 @@ var require_transport = __commonJS({
10750
10773
  return false;
10751
10774
  }
10752
10775
  }
10753
- return isAbsolute(path) && !existsSync4(path);
10776
+ return isAbsolute(path) && !existsSync3(path);
10754
10777
  }
10755
10778
  function stripQuotes(value) {
10756
10779
  const first = value[0];
@@ -76165,6 +76188,126 @@ export const test = base.extend({
76165
76188
  }
76166
76189
  });
76167
76190
 
76191
+ // ../runner-core/dist/services/run-callback-proxy.js
76192
+ var require_run_callback_proxy = __commonJS({
76193
+ "../runner-core/dist/services/run-callback-proxy.js"(exports2) {
76194
+ "use strict";
76195
+ Object.defineProperty(exports2, "__esModule", { value: true });
76196
+ exports2.startRunCallbackProxy = startRunCallbackProxy;
76197
+ var node_crypto_1 = require("crypto");
76198
+ var node_http_1 = require("http");
76199
+ var MAX_CALLBACK_BODY_BYTES = 5 * 1024 * 1024;
76200
+ var CALLBACK_TIMEOUT_MS = 3e4;
76201
+ function sendJson(res, status, body) {
76202
+ const payload = JSON.stringify(body);
76203
+ res.writeHead(status, {
76204
+ "cache-control": "no-store",
76205
+ "content-type": "application/json",
76206
+ "content-length": Buffer.byteLength(payload)
76207
+ });
76208
+ res.end(payload);
76209
+ }
76210
+ async function readRequestBody(req) {
76211
+ const chunks = [];
76212
+ let total = 0;
76213
+ for await (const chunk of req) {
76214
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
76215
+ total += buffer.length;
76216
+ if (total > MAX_CALLBACK_BODY_BYTES)
76217
+ throw new Error("callback body too large");
76218
+ chunks.push(buffer);
76219
+ }
76220
+ return Buffer.concat(chunks);
76221
+ }
76222
+ async function startRunCallbackProxy(input) {
76223
+ const upstream = new URL(input.serverUrl);
76224
+ if (upstream.protocol !== "http:" && upstream.protocol !== "https:") {
76225
+ throw new Error("callback server URL must use http or https");
76226
+ }
76227
+ const runPath = `/api/runner/runs/${encodeURIComponent(input.runId)}`;
76228
+ const allowedPaths = /* @__PURE__ */ new Set([`${runPath}/credential`, `${runPath}/auth-state`]);
76229
+ const token = (0, node_crypto_1.randomBytes)(32).toString("base64url");
76230
+ const authHeader = `Bearer ${token}`;
76231
+ const server3 = (0, node_http_1.createServer)(async (req, res) => {
76232
+ try {
76233
+ const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
76234
+ if (req.method !== "POST" || !allowedPaths.has(requestUrl.pathname) || requestUrl.search) {
76235
+ sendJson(res, 404, { error: "Callback route not available" });
76236
+ return;
76237
+ }
76238
+ if (req.headers.authorization !== authHeader) {
76239
+ sendJson(res, 401, { error: "Invalid callback capability" });
76240
+ return;
76241
+ }
76242
+ const body = await readRequestBody(req);
76243
+ let parsedBody;
76244
+ try {
76245
+ const parsed = JSON.parse(body.toString("utf8"));
76246
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
76247
+ throw new Error("invalid body");
76248
+ parsedBody = parsed;
76249
+ } catch {
76250
+ sendJson(res, 400, { error: "Callback body must be a JSON object" });
76251
+ return;
76252
+ }
76253
+ const upstreamBody = JSON.stringify({
76254
+ ...parsedBody,
76255
+ ...input.taskArn ? { taskArn: input.taskArn } : {},
76256
+ ...input.claimEpoch !== void 0 ? { claimEpoch: input.claimEpoch } : {},
76257
+ ...input.authConfigHash ? { authConfigHash: input.authConfigHash } : {}
76258
+ });
76259
+ const destination = new URL(requestUrl.pathname, upstream);
76260
+ const response = await fetch(destination, {
76261
+ method: "POST",
76262
+ headers: {
76263
+ Authorization: input.upstreamAuthHeader,
76264
+ "Content-Type": "application/json"
76265
+ },
76266
+ body: upstreamBody,
76267
+ redirect: "error",
76268
+ signal: AbortSignal.timeout(CALLBACK_TIMEOUT_MS)
76269
+ });
76270
+ const responseBody = Buffer.from(await response.arrayBuffer());
76271
+ res.writeHead(response.status, {
76272
+ "cache-control": "no-store",
76273
+ "content-type": response.headers.get("content-type") ?? "application/json",
76274
+ "content-length": responseBody.length
76275
+ });
76276
+ res.end(responseBody);
76277
+ } catch (error2) {
76278
+ const message = error2 instanceof Error && error2.message === "callback body too large" ? error2.message : "Callback forwarding failed";
76279
+ sendJson(res, message === "callback body too large" ? 413 : 502, { error: message });
76280
+ }
76281
+ });
76282
+ server3.on("clientError", (_error, socket) => socket.destroy());
76283
+ await new Promise((resolve2, reject) => {
76284
+ const onError = (error2) => reject(error2);
76285
+ server3.once("error", onError);
76286
+ server3.listen(0, "127.0.0.1", () => {
76287
+ server3.off("error", onError);
76288
+ resolve2();
76289
+ });
76290
+ });
76291
+ const address = server3.address();
76292
+ if (!address) {
76293
+ server3.close();
76294
+ throw new Error("callback proxy failed to bind");
76295
+ }
76296
+ let closed = false;
76297
+ return {
76298
+ serverUrl: `http://127.0.0.1:${address.port}`,
76299
+ authHeader,
76300
+ close: async () => {
76301
+ if (closed)
76302
+ return;
76303
+ closed = true;
76304
+ await new Promise((resolve2) => server3.close(() => resolve2()));
76305
+ }
76306
+ };
76307
+ }
76308
+ }
76309
+ });
76310
+
76168
76311
  // ../runner-core/dist/services/playwright-native.js
76169
76312
  var require_playwright_native = __commonJS({
76170
76313
  "../runner-core/dist/services/playwright-native.js"(exports2) {
@@ -76209,6 +76352,7 @@ var require_playwright_native = __commonJS({
76209
76352
  Object.defineProperty(exports2, "__esModule", { value: true });
76210
76353
  exports2.scrubHarRequestBody = exports2.buildOriginScopedApiFixtures = exports2.isAppOriginRequest = exports2.resolveApiAuthHeaders = exports2.parseApiAuthHeaders = void 0;
76211
76354
  exports2.executeTestViaNativePlaywright = executeTestViaNativePlaywright2;
76355
+ exports2.buildNativePlaywrightChildEnv = buildNativePlaywrightChildEnv;
76212
76356
  exports2.parseJsonResults = parseJsonResults2;
76213
76357
  exports2.parseHarToApiSummary = parseHarToApiSummary;
76214
76358
  var promises_1 = require("fs/promises");
@@ -76220,6 +76364,7 @@ var require_playwright_native = __commonJS({
76220
76364
  var api_call_redaction_js_1 = require_api_call_redaction();
76221
76365
  var har_stream_js_1 = require_har_stream();
76222
76366
  var api_auth_headers_js_1 = require_api_auth_headers();
76367
+ var run_callback_proxy_js_1 = require_run_callback_proxy();
76223
76368
  var api_auth_headers_js_2 = require_api_auth_headers();
76224
76369
  Object.defineProperty(exports2, "parseApiAuthHeaders", { enumerable: true, get: function() {
76225
76370
  return api_auth_headers_js_2.parseApiAuthHeaders;
@@ -76242,7 +76387,7 @@ var require_playwright_native = __commonJS({
76242
76387
  var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
76243
76388
  var PW_CLI = require.resolve("@playwright/test/cli");
76244
76389
  async function executeTestViaNativePlaywright2(options) {
76245
- const { playwrightCode, baseUrl, testName, testVariables, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
76390
+ const { playwrightCode, baseUrl, testName, testVariables, authStateCapturePath, storageStatePath, extraHttpHeaders, validateContext, liveBrowserHandle, onLiveBrowserSnapshot } = options;
76246
76391
  const logs2 = [];
76247
76392
  const screenshots = [];
76248
76393
  logs2.push(`\u{1F3AD} Native Playwright starting`);
@@ -76252,6 +76397,7 @@ var require_playwright_native = __commonJS({
76252
76397
  logs2.push("\u26A0\uFE0F WARNING: baseUrl is empty - test will likely fail with navigation errors");
76253
76398
  }
76254
76399
  let runDir = null;
76400
+ let callbackProxy;
76255
76401
  try {
76256
76402
  runDir = (0, path_1.join)(RUNS_DIR, (0, crypto_1.randomUUID)());
76257
76403
  await (0, promises_1.mkdir)(runDir, { recursive: true });
@@ -76511,35 +76657,51 @@ module.exports = {
76511
76657
  const configPath = (0, path_1.join)(runDir, "playwright.config.js");
76512
76658
  await (0, promises_1.writeFile)(configPath, config);
76513
76659
  const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(runDir);
76514
- const envWithIsolation = {
76515
- ...testVariables ?? {},
76660
+ const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
76661
+ const callbackClaimEpoch = Number.isSafeInteger(validateContext?.claimEpoch) ? validateContext?.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
76662
+ if (validateContext?.runId && validateContext.serverUrl && validateContext.authHeader) {
76663
+ try {
76664
+ callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
76665
+ runId: validateContext.runId,
76666
+ serverUrl: validateContext.serverUrl,
76667
+ upstreamAuthHeader: validateContext.authHeader,
76668
+ taskArn: process.env.VALIDATE_TASK_ARN,
76669
+ claimEpoch: callbackClaimEpoch,
76670
+ authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
76671
+ });
76672
+ } catch (error3) {
76673
+ logs2.push(`Callback proxy unavailable: ${error3 instanceof Error ? error3.message : String(error3)}`);
76674
+ }
76675
+ }
76676
+ const childTestVariables = { ...testVariables ?? {} };
76677
+ const trustedRuntimeEnv = {
76516
76678
  ...isolation.env,
76517
- // persistCredential() callback envs — set only when the runner supplied
76518
- // validateContext. Without these the helper inside the test no-ops with
76519
- // a warning, which is the correct behavior when the test is being run
76520
- // manually (outside the validate.qa harness).
76521
- ...validateContext?.runId ? { VALIDATE_RUN_ID: validateContext.runId } : {},
76522
- ...validateContext?.serverUrl ? { VALIDATE_SERVER_URL: validateContext.serverUrl } : {},
76523
- ...validateContext?.authHeader ? { VALIDATE_RUNNER_AUTH: validateContext.authHeader } : {},
76679
+ // Generated callbacks receive only a random loopback capability. The global
76680
+ // runner bearer token and real control-plane URL stay in the parent process.
76681
+ ...callbackProxy && validateContext?.runId ? {
76682
+ VALIDATE_RUN_ID: validateContext.runId,
76683
+ ...callbackClaimEpoch !== void 0 ? { VALIDATE_CLAIM_EPOCH: String(callbackClaimEpoch) } : {},
76684
+ VALIDATE_SERVER_URL: callbackProxy.serverUrl,
76685
+ VALIDATE_RUNNER_AUTH: callbackProxy.authHeader
76686
+ } : {},
76524
76687
  // Owning Fargate taskArn (a per-task constant set once at task boot in
76525
76688
  // execute-one.ts; absent on the local/Railway runner). Read from
76526
76689
  // process.env rather than validateContext since it's process-wide, not
76527
76690
  // per-run. Lets persistCredential() prove ownership to /credential.
76528
76691
  ...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {},
76692
+ ...authStateCapturePath ? { AUTH_FILE: authStateCapturePath } : {},
76529
76693
  ...liveBrowserStatePath ? { VALIDATE_LIVE_BROWSER_STATE_PATH: liveBrowserStatePath } : {},
76530
- // Shared-auth kill-switch (SHARED_AUTH_INJECT) must reach the child: the stamped
76531
- // __vqaLogin's early-publish POST is gated on it at RUNTIME. buildSandboxedEnv only
76532
- // copies an allowlist from process.env (SHARED_AUTH_INJECT is not on it), so without
76533
- // this forward the flag reads as undefined in the test and the early publish is dead
76534
- // code. Forwarded (not allowlisted) so it stays a per-run toggle; absent → inert.
76694
+ // Preserve an explicit shared-auth kill-switch in the child. Unset defaults on in
76695
+ // both parent and generated __vqaLogin code; "false" must still cross this process
76696
+ // boundary to disable the early publish POST. It remains a per-run toggle.
76535
76697
  ...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
76536
76698
  };
76537
- delete envWithIsolation.API_AUTH_HEADERS;
76699
+ delete childTestVariables.API_AUTH_HEADERS;
76538
76700
  logs2.push("\u{1F680} Spawning playwright test...");
76539
76701
  const stopLiveBrowserPolling = liveBrowserStatePath && liveBrowserHandle ? startLiveBrowserStatePolling(liveBrowserStatePath, liveBrowserHandle, onLiveBrowserSnapshot) : null;
76540
76702
  let playResult;
76541
76703
  try {
76542
- playResult = await runPlaywright(configPath, envWithIsolation);
76704
+ playResult = await runPlaywright(configPath, childTestVariables, trustedRuntimeEnv);
76543
76705
  } finally {
76544
76706
  await stopLiveBrowserPolling?.();
76545
76707
  }
@@ -76623,6 +76785,8 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76623
76785
  error: msg
76624
76786
  };
76625
76787
  } finally {
76788
+ await callbackProxy?.close().catch(() => {
76789
+ });
76626
76790
  if (runDir) {
76627
76791
  await (0, promises_1.rm)(runDir, { recursive: true, force: true }).catch(() => {
76628
76792
  });
@@ -76674,9 +76838,9 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76674
76838
  stopped = true;
76675
76839
  };
76676
76840
  }
76677
- function runPlaywright(configPath, extraEnv) {
76841
+ function runPlaywright(configPath, testVariables, trustedRuntimeEnv) {
76678
76842
  return new Promise((resolvePromise) => {
76679
- const sandboxedEnv = (0, shared_1.buildSandboxedEnv)(process.env, extraEnv);
76843
+ const sandboxedEnv = buildNativePlaywrightChildEnv(process.env, testVariables, trustedRuntimeEnv);
76680
76844
  const proc = (0, spawn_with_tree_kill_js_1.spawnDetachedGroup)(process.execPath, [PW_CLI, "test", "--config", configPath], {
76681
76845
  cwd: PACKAGE_DIR,
76682
76846
  env: sandboxedEnv,
@@ -76719,6 +76883,12 @@ ${passed ? "\u2705" : "\u274C"} Test ${passed ? "PASSED" : "FAILED"}${error2 ? `
76719
76883
  });
76720
76884
  });
76721
76885
  }
76886
+ function buildNativePlaywrightChildEnv(processEnv, testVariables, trustedRuntimeEnv) {
76887
+ return {
76888
+ ...(0, shared_1.buildSandboxedEnv)(processEnv, testVariables),
76889
+ ...trustedRuntimeEnv ?? {}
76890
+ };
76891
+ }
76722
76892
  async function collectScreenshots(dir, screenshots) {
76723
76893
  let entries;
76724
76894
  try {
@@ -80440,10 +80610,12 @@ var require_auth_recipe = __commonJS({
80440
80610
  exports2.upsertAuthBlock = upsertAuthBlock;
80441
80611
  exports2.buildHealAuthStamp = buildHealAuthStamp;
80442
80612
  var crypto_1 = require("crypto");
80613
+ var shared_1 = (init_dist(), __toCommonJS(dist_exports));
80443
80614
  var discovery_lint_flags_js_1 = require_discovery_lint_flags();
80444
- exports2.AUTH_MARKER_VERSION = "v1";
80615
+ exports2.AUTH_MARKER_VERSION = "v3";
80445
80616
  var START_MARKER = `// >>> VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} \u2014 generated, do not edit >>>`;
80446
80617
  var END_MARKER = `// <<< VALIDATE_QA_AUTH ${exports2.AUTH_MARKER_VERSION} <<<`;
80618
+ var SHARED_AUTH_RUNTIME_ENABLED = `!/^false$/i.test((process.env.SHARED_AUTH_INJECT ?? '').trim())`;
80447
80619
  var AUTH_REGION_RE = /\/\/ >>> VALIDATE_QA_AUTH[^\n]*\r?\n[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH[^\n]*\r?\n?/g;
80448
80620
  function computeRecipeHash(input) {
80449
80621
  const canonical = JSON.stringify({
@@ -80521,16 +80693,18 @@ var require_auth_recipe = __commonJS({
80521
80693
  if (opts?.warmGuard) {
80522
80694
  const warmLanding = recipe.redirectRoute ?? (recipe.authProof.kind === "url" ? recipe.authProof.route : recipe.preNavRoute);
80523
80695
  const loginRoute = recipe.route ?? recipe.preNavRoute;
80524
- lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
80696
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED}) {`);
80697
+ lines.push(` await page.goto(${JSON.stringify(warmLanding)}, { waitUntil: 'domcontentloaded' }).catch(() => {});`);
80525
80698
  if (recipe.authProof.kind === "url") {
80526
80699
  const pred = urlProofPredicateExpr(recipe.authProof.route);
80527
- lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
80528
- lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
80700
+ lines.push(` await page.waitForURL((u) => !(${pred})(u), { timeout: 2000 }).catch(() => {});`);
80701
+ lines.push(` if ((${pred})(new URL(page.url())) && !(await page.locator('input[type="password"]').first().isVisible().catch(() => false))) return;`);
80529
80702
  } else if (loginRoute) {
80530
- lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
80703
+ lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false) && new URL(page.url()).pathname !== new URL(${JSON.stringify(loginRoute)}, page.url()).pathname) return;`);
80531
80704
  } else {
80532
- lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
80705
+ lines.push(` if (await ${authProofLocator(recipe.authProof)}.isVisible().catch(() => false)) return;`);
80533
80706
  }
80707
+ lines.push(` }`);
80534
80708
  }
80535
80709
  lines.push(` await page.goto(${JSON.stringify(recipe.preNavRoute)}, { waitUntil: 'domcontentloaded' });`);
80536
80710
  for (const cmd of recipe.preNavCmds)
@@ -80543,11 +80717,11 @@ var require_auth_recipe = __commonJS({
80543
80717
  lines.push(renderAuthUrlWaitLine(recipe.redirectRoute));
80544
80718
  lines.push(` ${renderAuthProofAssertion(recipe.authProof)}`);
80545
80719
  if (opts?.warmGuard) {
80546
- lines.push(` if (process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
80547
- lines.push(` if (process.env.SHARED_AUTH_INJECT === 'true' && process.env.VALIDATE_TASK_ARN && process.env.VALIDATE_SERVER_URL && process.env.VALIDATE_RUN_ID) {`);
80720
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.AUTH_FILE) await page.context().storageState({ path: process.env.AUTH_FILE });`);
80721
+ lines.push(` if (${SHARED_AUTH_RUNTIME_ENABLED} && process.env.VALIDATE_TASK_ARN && process.env.VALIDATE_CLAIM_EPOCH && process.env.VALIDATE_SERVER_URL && process.env.VALIDATE_RUN_ID) {`);
80548
80722
  lines.push(` try {`);
80549
80723
  lines.push(` const __vqaState = await page.context().storageState();`);
80550
- lines.push(` await page.request.post(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + process.env.VALIDATE_RUN_ID + '/auth-state', { headers: { Authorization: process.env.VALIDATE_RUNNER_AUTH || '' }, data: { storageState: JSON.stringify(__vqaState), taskArn: process.env.VALIDATE_TASK_ARN }, timeout: 10000 });`);
80724
+ lines.push(` await page.request.post(process.env.VALIDATE_SERVER_URL + '/api/runner/runs/' + process.env.VALIDATE_RUN_ID + '/auth-state', { headers: { Authorization: process.env.VALIDATE_RUNNER_AUTH || '' }, data: { storageState: JSON.stringify(__vqaState), taskArn: process.env.VALIDATE_TASK_ARN, claimEpoch: Number(process.env.VALIDATE_CLAIM_EPOCH) }, timeout: 10000 });`);
80551
80725
  lines.push(` } catch {}`);
80552
80726
  lines.push(` }`);
80553
80727
  }
@@ -80559,6 +80733,8 @@ var require_auth_recipe = __commonJS({
80559
80733
  "AUTH_FILE",
80560
80734
  "SHARED_AUTH_INJECT",
80561
80735
  "VALIDATE_RUN_ID",
80736
+ "VALIDATE_CLAIM_EPOCH",
80737
+ "VALIDATE_AUTH_CONFIG_HASH",
80562
80738
  "VALIDATE_TASK_ARN",
80563
80739
  "VALIDATE_RUNNER_AUTH",
80564
80740
  "VALIDATE_SERVER_URL"
@@ -80731,7 +80907,7 @@ ${tail}`;
80731
80907
  let result = stripAuthRegions(code);
80732
80908
  result = result.replace(/^[ \t]*await[ \t]+__vqaLogin[ \t]*\([ \t]*page[ \t]*\)[ \t]*;?[ \t]*\r?\n/gm, "");
80733
80909
  result = ensureExpectImported(result);
80734
- const warmGuard = process.env.SHARED_AUTH_INJECT === "true";
80910
+ const warmGuard = (0, shared_1.isSharedAuthInjectEnabled)();
80735
80911
  result = insertAfterPwImport(result, renderAuthFn(ctx.recipe, { warmGuard }));
80736
80912
  result = ensureLoginCall(result);
80737
80913
  const n = countAuthRegions(result);
@@ -85861,9 +86037,9 @@ ${testOpen}`;
85861
86037
  basePath: options?.basePath
85862
86038
  });
85863
86039
  const leakedSensitiveKeys = findSensitiveLiteralLeaks(playwrightCode, options?.sensitiveEnvMap);
85864
- const authGuardStamped = !!options?.authRecipe && rawTags.includes("@needs-auth") && !rawTags.includes("@auth") && process.env.SHARED_AUTH_INJECT === "true";
85865
- const baseTags = ensureDiscoveredTag(Array.isArray(test.tags) ? test.tags : ["@discovered", "@regression"]);
85866
- const tags = authGuardStamped && !baseTags.includes("@auth-guarded") ? [...baseTags, "@auth-guarded"] : baseTags;
86040
+ 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);
86041
+ const baseTags = ensureDiscoveredTag(Array.isArray(test.tags) ? test.tags : ["@discovered", "@regression"]).filter((tag) => tag !== "@auth-guarded");
86042
+ const tags = authGuardStamped ? [...baseTags, "@auth-guarded"] : baseTags;
85867
86043
  return {
85868
86044
  name: String(test.name || "Unnamed test").slice(0, 200),
85869
86045
  description: String(test.description || test.name || "").slice(0, 500),
@@ -99678,7 +99854,7 @@ ${lanes.join("\n")}
99678
99854
  return process.memoryUsage().heapUsed;
99679
99855
  },
99680
99856
  getFileSize(path) {
99681
- const stat = statSync2(path);
99857
+ const stat = statSync(path);
99682
99858
  if (stat == null ? void 0 : stat.isFile()) {
99683
99859
  return stat.size;
99684
99860
  }
@@ -99722,7 +99898,7 @@ ${lanes.join("\n")}
99722
99898
  }
99723
99899
  };
99724
99900
  return nodeSystem;
99725
- function statSync2(path) {
99901
+ function statSync(path) {
99726
99902
  try {
99727
99903
  return _fs.statSync(path, statSyncOptions);
99728
99904
  } catch {
@@ -99781,7 +99957,7 @@ ${lanes.join("\n")}
99781
99957
  activeSession.post("Profiler.stop", (err, { profile }) => {
99782
99958
  var _a;
99783
99959
  if (!err) {
99784
- if ((_a = statSync2(profilePath)) == null ? void 0 : _a.isDirectory()) {
99960
+ if ((_a = statSync(profilePath)) == null ? void 0 : _a.isDirectory()) {
99785
99961
  profilePath = _path.join(profilePath, `${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`);
99786
99962
  }
99787
99963
  try {
@@ -99901,7 +100077,7 @@ ${lanes.join("\n")}
99901
100077
  let stat;
99902
100078
  if (typeof dirent === "string" || dirent.isSymbolicLink()) {
99903
100079
  const name = combinePaths(path, entry);
99904
- stat = statSync2(name);
100080
+ stat = statSync(name);
99905
100081
  if (!stat) {
99906
100082
  continue;
99907
100083
  }
@@ -99925,7 +100101,7 @@ ${lanes.join("\n")}
99925
100101
  return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames2, process.cwd(), depth, getAccessibleFileSystemEntries, realpath);
99926
100102
  }
99927
100103
  function fileSystemEntryExists(path, entryKind) {
99928
- const stat = statSync2(path);
100104
+ const stat = statSync(path);
99929
100105
  if (!stat) {
99930
100106
  return false;
99931
100107
  }
@@ -99967,7 +100143,7 @@ ${lanes.join("\n")}
99967
100143
  }
99968
100144
  function getModifiedTime3(path) {
99969
100145
  var _a;
99970
- return (_a = statSync2(path)) == null ? void 0 : _a.mtime;
100146
+ return (_a = statSync(path)) == null ? void 0 : _a.mtime;
99971
100147
  }
99972
100148
  function setModifiedTime(path, time) {
99973
100149
  try {
@@ -308478,6 +308654,7 @@ var require_regression_runner = __commonJS({
308478
308654
  var chromium_isolation_js_1 = require_chromium_isolation();
308479
308655
  var browser_config_js_1 = require_browser_config();
308480
308656
  var spawn_with_tree_kill_js_1 = require_spawn_with_tree_kill();
308657
+ var run_callback_proxy_js_1 = require_run_callback_proxy();
308481
308658
  var SUITE_SPAWN_TIMEOUT_MS = 85 * 60 * 1e3;
308482
308659
  var PACKAGE_DIR = (0, path_1.resolve)(__dirname, "..", "..");
308483
308660
  var RUNS_DIR = process.env.RUNS_DIR || (0, path_1.join)(PACKAGE_DIR, ".qa-runs");
@@ -308492,6 +308669,7 @@ var require_regression_runner = __commonJS({
308492
308669
  const tarPath = (0, path_1.join)(RUNS_DIR, `regression-${runId}.tar.gz`);
308493
308670
  const startTime = Date.now();
308494
308671
  let logs2 = "";
308672
+ let callbackProxy;
308495
308673
  try {
308496
308674
  await (0, promises_1.mkdir)(extractDir, { recursive: true });
308497
308675
  const tarballUrl = `https://api.github.com/repos/${repoOwner}/${repoName}/tarball/${repoBranch}`;
@@ -308521,19 +308699,40 @@ var require_regression_runner = __commonJS({
308521
308699
  await (0, promises_1.writeFile)((0, path_1.join)(extractDir, ".env"), envContent, "utf-8");
308522
308700
  const jsonResultsPath = (0, path_1.join)(extractDir, "test-results", "results.json");
308523
308701
  const isolation = await (0, chromium_isolation_js_1.createChromiumIsolation)(extractDir);
308702
+ const processClaimEpoch = Number(process.env.CLAIM_EPOCH);
308703
+ const callbackClaimEpoch = Number.isSafeInteger(opts.claimEpoch) ? opts.claimEpoch : Number.isSafeInteger(processClaimEpoch) && processClaimEpoch >= 0 ? processClaimEpoch : void 0;
308704
+ if (opts.validateServerUrl && opts.validateRunnerAuthHeader) {
308705
+ try {
308706
+ callbackProxy = await (0, run_callback_proxy_js_1.startRunCallbackProxy)({
308707
+ runId,
308708
+ serverUrl: opts.validateServerUrl,
308709
+ upstreamAuthHeader: opts.validateRunnerAuthHeader,
308710
+ taskArn: process.env.VALIDATE_TASK_ARN,
308711
+ claimEpoch: callbackClaimEpoch,
308712
+ authConfigHash: process.env.VALIDATE_AUTH_CONFIG_HASH
308713
+ });
308714
+ } catch (error2) {
308715
+ logs2 += `Callback proxy unavailable: ${error2 instanceof Error ? error2.message : String(error2)}
308716
+ `;
308717
+ }
308718
+ }
308524
308719
  const runEnv = {
308525
308720
  ...(0, shared_1.buildSandboxedEnv)(process.env, testVariables),
308526
308721
  ...isolation.env,
308527
308722
  CI: "true",
308528
308723
  PLAYWRIGHT_JSON_OUTPUT_NAME: jsonResultsPath,
308529
- // Callback envs for tests/setup/credentials.ts → persistCredential.
308530
- // Tests that rotate a credential POST back to the server using these.
308531
- VALIDATE_RUN_ID: runId,
308532
- ...opts.validateServerUrl ? { VALIDATE_SERVER_URL: opts.validateServerUrl } : {},
308533
- ...opts.validateRunnerAuthHeader ? { VALIDATE_RUNNER_AUTH: opts.validateRunnerAuthHeader } : {},
308724
+ // The suite receives a random run-scoped loopback callback, never the
308725
+ // global runner bearer credential.
308726
+ ...callbackProxy ? {
308727
+ VALIDATE_RUN_ID: runId,
308728
+ ...callbackClaimEpoch !== void 0 ? { VALIDATE_CLAIM_EPOCH: String(callbackClaimEpoch) } : {},
308729
+ VALIDATE_SERVER_URL: callbackProxy.serverUrl,
308730
+ VALIDATE_RUNNER_AUTH: callbackProxy.authHeader
308731
+ } : {},
308534
308732
  // Owning Fargate taskArn (per-task constant; unset off-Fargate). Forwarded
308535
308733
  // so persistCredential() can prove ownership to the /credential fence.
308536
- ...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {}
308734
+ ...process.env.VALIDATE_TASK_ARN ? { VALIDATE_TASK_ARN: process.env.VALIDATE_TASK_ARN } : {},
308735
+ ...process.env.SHARED_AUTH_INJECT ? { SHARED_AUTH_INJECT: process.env.SHARED_AUTH_INJECT } : {}
308537
308736
  };
308538
308737
  logs2 += `Running playwright --project=${opts.suiteProject ?? "suite"}...
308539
308738
  `;
@@ -308562,6 +308761,8 @@ var require_regression_runner = __commonJS({
308562
308761
  error: String(err)
308563
308762
  };
308564
308763
  } finally {
308764
+ await callbackProxy?.close().catch(() => {
308765
+ });
308565
308766
  (0, promises_1.rm)(extractDir, { recursive: true, force: true }).catch(() => {
308566
308767
  });
308567
308768
  await (0, browser_config_js_1.cleanupPlaywrightTempArtifacts)().catch(() => {
@@ -324805,6 +325006,455 @@ var require_resource_governor = __commonJS({
324805
325006
  }
324806
325007
  });
324807
325008
 
325009
+ // ../runner-core/dist/services/local-auth-state.js
325010
+ var require_local_auth_state = __commonJS({
325011
+ "../runner-core/dist/services/local-auth-state.js"(exports2) {
325012
+ "use strict";
325013
+ Object.defineProperty(exports2, "__esModule", { value: true });
325014
+ exports2.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS = exports2.LOCAL_AUTH_STATE_LOCK_STALE_MS = exports2.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS = exports2.LOCAL_AUTH_STATE_WAIT_MS = exports2.LOCAL_AUTH_STATE_MAX_AGE_MS = void 0;
325015
+ exports2.shouldSkipWarmAuthSetup = shouldSkipWarmAuthSetup2;
325016
+ exports2.shouldSkipDisabledAutomaticAuthSetup = shouldSkipDisabledAutomaticAuthSetup2;
325017
+ exports2.localAuthStateFingerprint = localAuthStateFingerprint;
325018
+ exports2.ensurePrivateDirectory = ensurePrivateDirectory2;
325019
+ exports2.isValidStorageState = isValidStorageState;
325020
+ exports2.isValidStorageStateJson = isValidStorageStateJson;
325021
+ exports2.isValidLocalAuthStateFile = isValidLocalAuthStateFile;
325022
+ exports2.cleanupLocalAuthStateFiles = cleanupLocalAuthStateFiles;
325023
+ exports2.cleanupExpiredLocalAuthStateFiles = cleanupExpiredLocalAuthStateFiles2;
325024
+ exports2.localAuthStatePaths = localAuthStatePaths2;
325025
+ exports2.getValidLocalAuthState = getValidLocalAuthState2;
325026
+ exports2.waitForValidLocalAuthState = waitForValidLocalAuthState;
325027
+ exports2.waitForLocalAuthStateProducer = waitForLocalAuthStateProducer2;
325028
+ exports2.promoteLocalAuthStateCapture = promoteLocalAuthStateCapture2;
325029
+ exports2.discardLocalAuthStateCapture = discardLocalAuthStateCapture2;
325030
+ exports2.acquireLocalAuthStateLock = acquireLocalAuthStateLock2;
325031
+ exports2.localAuthStateFilename = localAuthStateFilename;
325032
+ var node_fs_1 = require("fs");
325033
+ var node_crypto_1 = require("crypto");
325034
+ var node_path_1 = require("path");
325035
+ var auth_recipe_js_1 = require_auth_recipe();
325036
+ exports2.LOCAL_AUTH_STATE_MAX_AGE_MS = 60 * 60 * 1e3;
325037
+ exports2.LOCAL_AUTH_STATE_WAIT_MS = 3 * 60 * 1e3;
325038
+ exports2.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS = 15 * 60 * 1e3;
325039
+ exports2.LOCAL_AUTH_STATE_LOCK_STALE_MS = 2 * 60 * 1e3;
325040
+ exports2.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS = 30 * 60 * 1e3;
325041
+ function shouldSkipWarmAuthSetup2(authForceRun, sharedAuthEnabled, cacheWarm) {
325042
+ return sharedAuthEnabled && authForceRun !== true && cacheWarm;
325043
+ }
325044
+ function shouldSkipDisabledAutomaticAuthSetup2(authForceRun, authVerify, sharedAuthEnabled) {
325045
+ return !sharedAuthEnabled && authForceRun !== true && authVerify !== true;
325046
+ }
325047
+ function normalizeOrigin(value) {
325048
+ try {
325049
+ return new URL(value).origin.toLowerCase();
325050
+ } catch {
325051
+ return value.trim().toLowerCase();
325052
+ }
325053
+ }
325054
+ function normalizeAppUrl(value) {
325055
+ try {
325056
+ const url = new URL(value);
325057
+ url.hash = "";
325058
+ return `${url.origin.toLowerCase()}${url.pathname}${url.search}`;
325059
+ } catch {
325060
+ return value.trim();
325061
+ }
325062
+ }
325063
+ function safeSegment(value, fallback) {
325064
+ const safe = value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 120);
325065
+ return safe || fallback;
325066
+ }
325067
+ function stableVariables(testVariables) {
325068
+ return Object.entries(testVariables).filter(([key]) => !(0, auth_recipe_js_1.isReservedRunnerEnvVar)(key)).sort(([a], [b]) => a.localeCompare(b));
325069
+ }
325070
+ function localAuthStateFingerprint(input) {
325071
+ const payload = JSON.stringify({
325072
+ version: 1,
325073
+ projectId: input.projectId,
325074
+ serverOrigin: normalizeOrigin(input.serverUrl),
325075
+ appUrl: normalizeAppUrl(input.baseUrl),
325076
+ testVariables: stableVariables(input.testVariables)
325077
+ });
325078
+ const key = input.authToken || `validate-local-auth:${input.projectId}`;
325079
+ return (0, node_crypto_1.createHmac)("sha256", key).update(payload).digest("hex").slice(0, 24);
325080
+ }
325081
+ function ensurePrivateDirectory2(dir) {
325082
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 448 });
325083
+ try {
325084
+ (0, node_fs_1.chmodSync)(dir, 448);
325085
+ } catch {
325086
+ }
325087
+ }
325088
+ function removeFile(path) {
325089
+ try {
325090
+ (0, node_fs_1.unlinkSync)(path);
325091
+ } catch {
325092
+ }
325093
+ }
325094
+ function readLockOwner(path) {
325095
+ try {
325096
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
325097
+ 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;
325098
+ } catch {
325099
+ return void 0;
325100
+ }
325101
+ }
325102
+ function processIsAlive(pid) {
325103
+ try {
325104
+ process.kill(pid, 0);
325105
+ return true;
325106
+ } catch (error2) {
325107
+ return error2.code === "EPERM";
325108
+ }
325109
+ }
325110
+ function staleLockCanBeReaped(path, now, staleMs) {
325111
+ try {
325112
+ const ageMs = now - (0, node_fs_1.statSync)(path).mtimeMs;
325113
+ if (ageMs < staleMs)
325114
+ return false;
325115
+ const owner = readLockOwner(path);
325116
+ if (owner) {
325117
+ if (!processIsAlive(owner.pid))
325118
+ return true;
325119
+ return ageMs >= Math.max(staleMs * 5, exports2.LOCAL_AUTH_STATE_LOCK_ABSOLUTE_STALE_MS);
325120
+ }
325121
+ return ageMs >= staleMs * 5;
325122
+ } catch {
325123
+ return false;
325124
+ }
325125
+ }
325126
+ function isRecord(value) {
325127
+ return !!value && typeof value === "object" && !Array.isArray(value);
325128
+ }
325129
+ function isValidStorageState(value) {
325130
+ if (!isRecord(value))
325131
+ return false;
325132
+ const cookies = value.cookies;
325133
+ const origins = value.origins;
325134
+ if (!Array.isArray(cookies) || !Array.isArray(origins))
325135
+ return false;
325136
+ const cookiesValid = cookies.every((cookie) => {
325137
+ if (!isRecord(cookie))
325138
+ return false;
325139
+ 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");
325140
+ });
325141
+ if (!cookiesValid)
325142
+ return false;
325143
+ const originsValid = origins.every((origin) => {
325144
+ if (!isRecord(origin) || typeof origin.origin !== "string" || !Array.isArray(origin.localStorage))
325145
+ return false;
325146
+ return origin.localStorage.every((entry) => isRecord(entry) && typeof entry.name === "string" && typeof entry.value === "string");
325147
+ });
325148
+ if (!originsValid)
325149
+ return false;
325150
+ return cookies.length > 0 || origins.some((origin) => origin.localStorage.length > 0);
325151
+ }
325152
+ function isValidStorageStateJson(value) {
325153
+ try {
325154
+ return isValidStorageState(JSON.parse(value));
325155
+ } catch {
325156
+ return false;
325157
+ }
325158
+ }
325159
+ function isStorageStateShape(value) {
325160
+ if (!value || typeof value !== "object")
325161
+ return false;
325162
+ return isValidStorageState(value);
325163
+ }
325164
+ function isValidLocalAuthStateFile(path) {
325165
+ try {
325166
+ return isStorageStateShape(JSON.parse((0, node_fs_1.readFileSync)(path, "utf8")));
325167
+ } catch {
325168
+ return false;
325169
+ }
325170
+ }
325171
+ function cleanupLocalAuthStateFiles(runsDir, projectId, now = Date.now(), maxAgeMs = exports2.LOCAL_AUTH_STATE_MAX_AGE_MS) {
325172
+ ensurePrivateDirectory2(runsDir);
325173
+ const projectSegment = safeSegment(projectId, "default");
325174
+ const legacyName = `auth-state-${projectSegment}.json`;
325175
+ const prefix = `auth-state-${projectSegment}-`;
325176
+ for (const name of (0, node_fs_1.readdirSync)(runsDir)) {
325177
+ if (name === legacyName) {
325178
+ removeFile((0, node_path_1.join)(runsDir, name));
325179
+ continue;
325180
+ }
325181
+ if (!name.startsWith(prefix))
325182
+ continue;
325183
+ const path = (0, node_path_1.join)(runsDir, name);
325184
+ try {
325185
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs) {
325186
+ if (!name.endsWith(".lock") || staleLockCanBeReaped(path, now, maxAgeMs))
325187
+ removeFile(path);
325188
+ }
325189
+ } catch {
325190
+ }
325191
+ }
325192
+ }
325193
+ function cleanupExpiredLocalAuthStateFiles2(runsDir, now = Date.now(), maxAgeMs = exports2.LOCAL_AUTH_STATE_MAX_AGE_MS) {
325194
+ ensurePrivateDirectory2(runsDir);
325195
+ for (const name of (0, node_fs_1.readdirSync)(runsDir)) {
325196
+ if (!name.startsWith("auth-state-"))
325197
+ continue;
325198
+ const path = (0, node_path_1.join)(runsDir, name);
325199
+ try {
325200
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs) {
325201
+ if (!name.endsWith(".lock") || staleLockCanBeReaped(path, now, maxAgeMs))
325202
+ removeFile(path);
325203
+ }
325204
+ } catch {
325205
+ }
325206
+ }
325207
+ }
325208
+ function localAuthStatePaths2(runsDir, input, runId) {
325209
+ cleanupLocalAuthStateFiles(runsDir, input.projectId);
325210
+ const projectSegment = safeSegment(input.projectId, "default");
325211
+ const runSegment = safeSegment(runId, `${process.pid}`);
325212
+ const fingerprint = localAuthStateFingerprint(input);
325213
+ const cachePath = (0, node_path_1.join)(runsDir, `auth-state-${projectSegment}-${fingerprint}.json`);
325214
+ return {
325215
+ cachePath,
325216
+ capturePath: `${cachePath}.capture-${runSegment}-${process.pid}.tmp`,
325217
+ fingerprint,
325218
+ lockKey: `${normalizeOrigin(input.serverUrl)}:${projectSegment}:${fingerprint}`,
325219
+ lockPath: `${cachePath}.lock`
325220
+ };
325221
+ }
325222
+ function getValidLocalAuthState2(path, now = Date.now(), maxAgeMs = exports2.LOCAL_AUTH_STATE_MAX_AGE_MS) {
325223
+ if (!(0, node_fs_1.existsSync)(path))
325224
+ return void 0;
325225
+ try {
325226
+ if (now - (0, node_fs_1.statSync)(path).mtimeMs >= maxAgeMs || !isValidLocalAuthStateFile(path)) {
325227
+ removeFile(path);
325228
+ return void 0;
325229
+ }
325230
+ return path;
325231
+ } catch {
325232
+ return void 0;
325233
+ }
325234
+ }
325235
+ async function waitForValidLocalAuthState(path, timeoutMs = exports2.LOCAL_AUTH_STATE_WAIT_MS, pollMs = 100) {
325236
+ const deadline = Date.now() + Math.max(0, timeoutMs);
325237
+ do {
325238
+ const valid = getValidLocalAuthState2(path);
325239
+ if (valid)
325240
+ return valid;
325241
+ const remaining = deadline - Date.now();
325242
+ if (remaining <= 0)
325243
+ return void 0;
325244
+ await new Promise((resolve2) => setTimeout(resolve2, Math.min(Math.max(10, pollMs), remaining)));
325245
+ } while (true);
325246
+ }
325247
+ async function waitForLocalAuthStateProducer2(path, lockKey, startupGraceMs = 2e3, maxWaitMs = exports2.LOCAL_AUTH_STATE_WAIT_MS, pollMs = 100, lockPath) {
325248
+ const startedAt = Date.now();
325249
+ const producerIsActive = () => lockTails.has(lockKey) || !!lockPath && (0, node_fs_1.existsSync)(lockPath);
325250
+ let sawProducer = producerIsActive();
325251
+ do {
325252
+ const valid = getValidLocalAuthState2(path);
325253
+ if (valid)
325254
+ return valid;
325255
+ const producerActive = producerIsActive();
325256
+ if (producerActive)
325257
+ sawProducer = true;
325258
+ const elapsed = Date.now() - startedAt;
325259
+ if (sawProducer && !producerActive)
325260
+ return void 0;
325261
+ if (!sawProducer && elapsed >= Math.max(0, startupGraceMs))
325262
+ return void 0;
325263
+ if (elapsed >= Math.max(0, maxWaitMs))
325264
+ return void 0;
325265
+ await new Promise((resolve2) => setTimeout(resolve2, Math.max(10, pollMs)));
325266
+ } while (true);
325267
+ }
325268
+ function promoteLocalAuthStateCapture2(capturePath, cachePath, replaceExisting = false, lease) {
325269
+ if (lease && !lease.isOwner()) {
325270
+ removeFile(capturePath);
325271
+ return false;
325272
+ }
325273
+ if (!(0, node_fs_1.existsSync)(capturePath) || !isValidLocalAuthStateFile(capturePath)) {
325274
+ removeFile(capturePath);
325275
+ return false;
325276
+ }
325277
+ try {
325278
+ (0, node_fs_1.chmodSync)(capturePath, 384);
325279
+ } catch {
325280
+ }
325281
+ try {
325282
+ if (lease && !lease.isOwner()) {
325283
+ removeFile(capturePath);
325284
+ return false;
325285
+ }
325286
+ if ((0, node_fs_1.existsSync)(cachePath)) {
325287
+ if (!replaceExisting && isValidLocalAuthStateFile(cachePath)) {
325288
+ removeFile(capturePath);
325289
+ return true;
325290
+ }
325291
+ removeFile(cachePath);
325292
+ }
325293
+ if (lease && !lease.isOwner()) {
325294
+ removeFile(capturePath);
325295
+ return false;
325296
+ }
325297
+ (0, node_fs_1.renameSync)(capturePath, cachePath);
325298
+ try {
325299
+ (0, node_fs_1.chmodSync)(cachePath, 384);
325300
+ } catch {
325301
+ }
325302
+ return true;
325303
+ } catch {
325304
+ const winnerIsValid = (0, node_fs_1.existsSync)(cachePath) && isValidLocalAuthStateFile(cachePath);
325305
+ removeFile(capturePath);
325306
+ return winnerIsValid;
325307
+ }
325308
+ }
325309
+ function discardLocalAuthStateCapture2(capturePath) {
325310
+ removeFile(capturePath);
325311
+ }
325312
+ var lockTails = /* @__PURE__ */ new Map();
325313
+ function publishCompleteLockOwner(lockPath, owner, linkFile) {
325314
+ const metadata = JSON.stringify(owner);
325315
+ const tempPath = `${lockPath}.${owner.pid}.${owner.token}.tmp`;
325316
+ let createdLock = false;
325317
+ try {
325318
+ (0, node_fs_1.writeFileSync)(tempPath, metadata, { flag: "wx", mode: 384 });
325319
+ try {
325320
+ linkFile(tempPath, lockPath);
325321
+ createdLock = true;
325322
+ return (0, node_fs_1.openSync)(lockPath, "r+");
325323
+ } catch (error2) {
325324
+ const code = error2.code;
325325
+ if (!["EPERM", "EACCES", "EXDEV", "ENOSYS", "ENOTSUP", "EOPNOTSUPP"].includes(code ?? "")) {
325326
+ throw error2;
325327
+ }
325328
+ let fd;
325329
+ try {
325330
+ fd = (0, node_fs_1.openSync)(lockPath, "wx+", 384);
325331
+ createdLock = true;
325332
+ (0, node_fs_1.writeFileSync)(fd, metadata, { encoding: "utf8" });
325333
+ (0, node_fs_1.fsyncSync)(fd);
325334
+ return fd;
325335
+ } catch (fallbackError) {
325336
+ if (fd !== void 0) {
325337
+ try {
325338
+ (0, node_fs_1.closeSync)(fd);
325339
+ } catch {
325340
+ }
325341
+ }
325342
+ if (createdLock)
325343
+ removeFile(lockPath);
325344
+ throw fallbackError;
325345
+ }
325346
+ }
325347
+ } catch (error2) {
325348
+ if (createdLock && readLockOwner(lockPath)?.token === owner.token)
325349
+ removeFile(lockPath);
325350
+ throw error2;
325351
+ } finally {
325352
+ removeFile(tempPath);
325353
+ }
325354
+ }
325355
+ async function acquireExclusiveFileLock(lockPath, linkFile = node_fs_1.linkSync) {
325356
+ ensurePrivateDirectory2((0, node_path_1.dirname)(lockPath));
325357
+ const startedAt = Date.now();
325358
+ while (true) {
325359
+ try {
325360
+ const ownerToken = (0, node_crypto_1.randomBytes)(24).toString("base64url");
325361
+ const fd = publishCompleteLockOwner(lockPath, { token: ownerToken, pid: process.pid }, linkFile);
325362
+ const ownsPath = () => {
325363
+ try {
325364
+ const owner = readLockOwner(lockPath);
325365
+ return owner?.token === ownerToken && owner.pid === process.pid;
325366
+ } catch {
325367
+ return false;
325368
+ }
325369
+ };
325370
+ const heartbeat = setInterval(() => {
325371
+ try {
325372
+ if (!ownsPath()) {
325373
+ clearInterval(heartbeat);
325374
+ return;
325375
+ }
325376
+ const now = /* @__PURE__ */ new Date();
325377
+ (0, node_fs_1.futimesSync)(fd, now, now);
325378
+ } catch {
325379
+ clearInterval(heartbeat);
325380
+ }
325381
+ }, Math.max(1e3, Math.floor(exports2.LOCAL_AUTH_STATE_LOCK_STALE_MS / 3)));
325382
+ heartbeat.unref();
325383
+ let released = false;
325384
+ const release = (() => {
325385
+ if (released)
325386
+ return;
325387
+ released = true;
325388
+ clearInterval(heartbeat);
325389
+ try {
325390
+ if (ownsPath())
325391
+ removeFile(lockPath);
325392
+ } catch {
325393
+ }
325394
+ try {
325395
+ (0, node_fs_1.closeSync)(fd);
325396
+ } catch {
325397
+ }
325398
+ });
325399
+ release.isOwner = () => !released && ownsPath();
325400
+ return release;
325401
+ } catch (error2) {
325402
+ const code = error2.code;
325403
+ if (code !== "EEXIST")
325404
+ throw error2;
325405
+ try {
325406
+ if (staleLockCanBeReaped(lockPath, Date.now(), exports2.LOCAL_AUTH_STATE_LOCK_STALE_MS)) {
325407
+ removeFile(lockPath);
325408
+ continue;
325409
+ }
325410
+ } catch {
325411
+ continue;
325412
+ }
325413
+ if (Date.now() - startedAt >= exports2.LOCAL_AUTH_STATE_WAIT_MS) {
325414
+ throw new Error(`Timed out waiting for auth-state lock: ${(0, node_path_1.basename)(lockPath)}`);
325415
+ }
325416
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
325417
+ }
325418
+ }
325419
+ }
325420
+ async function acquireLocalAuthStateLock2(key, lockPath, linkFile = node_fs_1.linkSync) {
325421
+ const previous = lockTails.get(key) ?? Promise.resolve();
325422
+ let unlockCurrent;
325423
+ const current = new Promise((resolve2) => {
325424
+ unlockCurrent = resolve2;
325425
+ });
325426
+ const tail = previous.then(() => current);
325427
+ lockTails.set(key, tail);
325428
+ await previous;
325429
+ let releaseFileLock;
325430
+ try {
325431
+ if (lockPath)
325432
+ releaseFileLock = await acquireExclusiveFileLock(lockPath, linkFile);
325433
+ } catch (error2) {
325434
+ unlockCurrent();
325435
+ if (lockTails.get(key) === tail)
325436
+ lockTails.delete(key);
325437
+ throw error2;
325438
+ }
325439
+ let released = false;
325440
+ const release = (() => {
325441
+ if (released)
325442
+ return;
325443
+ released = true;
325444
+ releaseFileLock?.();
325445
+ unlockCurrent();
325446
+ if (lockTails.get(key) === tail)
325447
+ lockTails.delete(key);
325448
+ });
325449
+ release.isOwner = () => !released && (releaseFileLock?.isOwner() ?? true);
325450
+ return release;
325451
+ }
325452
+ function localAuthStateFilename(path) {
325453
+ return (0, node_path_1.basename)(path);
325454
+ }
325455
+ }
325456
+ });
325457
+
324808
325458
  // ../../node_modules/adm-zip/util/constants.js
324809
325459
  var require_constants5 = __commonJS({
324810
325460
  "../../node_modules/adm-zip/util/constants.js"(exports2, module2) {
@@ -325118,7 +325768,7 @@ var require_utils2 = __commonJS({
325118
325768
  module2.exports = Utils;
325119
325769
  Utils.prototype.makeDir = function(folder) {
325120
325770
  const self = this;
325121
- function mkdirSync5(fpath) {
325771
+ function mkdirSync4(fpath) {
325122
325772
  let resolvedPath = fpath.split(self.sep)[0];
325123
325773
  fpath.split(self.sep).forEach(function(name) {
325124
325774
  if (!name || name.substr(-1, 1) === ":") return;
@@ -325136,7 +325786,7 @@ var require_utils2 = __commonJS({
325136
325786
  if (stat && stat.isFile()) throw Errors.FILE_IN_THE_WAY(`"${resolvedPath}"`);
325137
325787
  });
325138
325788
  }
325139
- mkdirSync5(folder);
325789
+ mkdirSync4(folder);
325140
325790
  };
325141
325791
  Utils.prototype.writeFileTo = function(path, content, overwrite, attr) {
325142
325792
  const self = this;
@@ -337060,6 +337710,8 @@ var require_dist2 = __commonJS({
337060
337710
  __exportStar(require_discover_agent(), exports2);
337061
337711
  __exportStar(require_browser_pool(), exports2);
337062
337712
  __exportStar(require_resource_governor(), exports2);
337713
+ __exportStar(require_local_auth_state(), exports2);
337714
+ __exportStar(require_run_callback_proxy(), exports2);
337063
337715
  __exportStar(require_discover_explorer(), exports2);
337064
337716
  __exportStar(require_discover_feature_explorer(), exports2);
337065
337717
  __exportStar(require_discover_planner(), exports2);
@@ -337336,7 +337988,7 @@ var require_package4 = __commonJS({
337336
337988
  "package.json"(exports2, module2) {
337337
337989
  module2.exports = {
337338
337990
  name: "@validate.qa/runner",
337339
- version: "1.0.42",
337991
+ version: "1.0.43",
337340
337992
  description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
337341
337993
  bin: {
337342
337994
  "validate-runner": "dist/cli.js"
@@ -337344,7 +337996,7 @@ var require_package4 = __commonJS({
337344
337996
  scripts: {
337345
337997
  build: "tsup",
337346
337998
  dev: "tsx src/cli.ts",
337347
- test: "node --import tsx --test src/version-check.test.ts",
337999
+ test: "node --import tsx --test src/version-check.test.ts src/services/auth-state.test.ts",
337348
338000
  "test:mobile": "node --import tsx --test --test-concurrency=2 src/runner.mobile-ai-env.test.ts src/mobile-device-queue.test.ts src/device-lease.test.ts src/result-payload-budget.test.ts src/services/appium-*.test.ts src/services/*mobile*.test.ts src/services/mobile/*.test.ts",
337349
338001
  prepublishOnly: "npm run build"
337350
338002
  },
@@ -337423,7 +338075,6 @@ function getConfigDir() {
337423
338075
  }
337424
338076
 
337425
338077
  // src/runner.ts
337426
- var import_fs5 = require("fs");
337427
338078
  var import_path4 = require("path");
337428
338079
  var import_os6 = require("os");
337429
338080
  var import_node_crypto2 = require("crypto");
@@ -337451,23 +338102,33 @@ var import_runner_core6 = __toESM(require_dist2());
337451
338102
 
337452
338103
  // src/runner.ts
337453
338104
  var import_runner_core11 = __toESM(require_dist2());
338105
+ var import_runner_core12 = __toESM(require_dist2());
337454
338106
 
337455
338107
  // src/services/auth-state.ts
338108
+ init_dist();
337456
338109
  var INLINE_LOGIN_TAG = "@inline-login";
337457
338110
  var GUARDED_LOGIN_TAG = "@auth-guarded";
337458
338111
  function sharedAuthInjectEnabled() {
337459
- return process.env.SHARED_AUTH_INJECT === "true";
338112
+ return isSharedAuthInjectEnabled();
338113
+ }
338114
+ function hasGeneratedAuthGuard(playwrightCode) {
338115
+ if (!playwrightCode) return false;
338116
+ return /\/\/ >>> VALIDATE_QA_AUTH v\d+\b[\s\S]*?process\.env\.AUTH_FILE[\s\S]*?\/\/ <<< VALIDATE_QA_AUTH v\d+\b/.test(playwrightCode);
338117
+ }
338118
+ function shouldCaptureAuthState(tags, playwrightCode) {
338119
+ return sharedAuthInjectEnabled() && Array.isArray(tags) && tags.includes("@needs-auth") && tags.includes(GUARDED_LOGIN_TAG) && !tags.includes("@auth") && hasGeneratedAuthGuard(playwrightCode);
337460
338120
  }
337461
338121
  function hasInlineLoginSteps(playwrightCode) {
337462
338122
  if (!playwrightCode) return false;
337463
338123
  return /process\.env\.TEST_USER_EMAIL!?/.test(playwrightCode) && /process\.env\.TEST_USER_PASSWORD!?/.test(playwrightCode);
337464
338124
  }
337465
338125
  function shouldUseAuthState(tags, playwrightCode) {
338126
+ if (!sharedAuthInjectEnabled()) return false;
337466
338127
  if (!Array.isArray(tags)) return false;
337467
338128
  if (tags.includes("@auth")) return false;
337468
338129
  if (tags.includes("@api")) return true;
337469
338130
  if (!tags.includes("@needs-auth")) return false;
337470
- if (sharedAuthInjectEnabled() && tags.includes(GUARDED_LOGIN_TAG)) return true;
338131
+ if (tags.includes(GUARDED_LOGIN_TAG) && hasGeneratedAuthGuard(playwrightCode)) return true;
337471
338132
  const isGeneratedVariant = tags.includes("@edge") || tags.includes("@bug");
337472
338133
  const hasLegacyInlineLogin = tags.includes("@discovered") && !isGeneratedVariant && hasInlineLoginSteps(playwrightCode);
337473
338134
  return !tags.includes(INLINE_LOGIN_TAG) && !hasLegacyInlineLogin;
@@ -344177,8 +344838,8 @@ function withMediaReferences(result, refs) {
344177
344838
 
344178
344839
  // src/runner.ts
344179
344840
  init_dist();
344180
- var import_runner_core12 = __toESM(require_dist2());
344181
344841
  var import_runner_core13 = __toESM(require_dist2());
344842
+ var import_runner_core14 = __toESM(require_dist2());
344182
344843
  function fingerprintTest(test) {
344183
344844
  const code = test.framework === "APPIUM" ? [test.appiumCode ?? "", JSON.stringify(test.steps ?? [])].join("\n--steps--\n") : test.playwrightCode ?? "";
344184
344845
  return (0, import_node_crypto2.createHash)("sha256").update(test.name).update("\n--suite--\n").update(test.suiteName ?? "").update("\n--type--\n").update(test.testType ?? "").update("\n--framework--\n").update(test.framework ?? "").update("\n--code--\n").update(code).digest("hex").slice(0, 24);
@@ -344302,21 +344963,11 @@ var log = {
344302
344963
  warn: (msg) => console.log(`${C2.yellow}\u26A0${C2.reset} ${msg}`),
344303
344964
  dim: (msg) => console.log(`${C2.gray}${msg}${C2.reset}`)
344304
344965
  };
344305
- var AUTH_MAX_AGE_MS = 60 * 60 * 1e3;
344306
344966
  function getRunsDir() {
344307
344967
  const dir = process.env.RUNS_DIR || (0, import_path4.join)((0, import_os6.homedir)(), ".validate.qa", "runs");
344308
- if (!(0, import_fs5.existsSync)(dir)) (0, import_fs5.mkdirSync)(dir, { recursive: true });
344968
+ (0, import_runner_core12.ensurePrivateDirectory)(dir);
344309
344969
  return dir;
344310
344970
  }
344311
- function authStatePath(projectId) {
344312
- return (0, import_path4.join)(getRunsDir(), `auth-state-${projectId || "default"}.json`);
344313
- }
344314
- function getValidAuthState(projectId) {
344315
- const path = authStatePath(projectId);
344316
- if (!(0, import_fs5.existsSync)(path)) return void 0;
344317
- const age = Date.now() - (0, import_fs5.statSync)(path).mtimeMs;
344318
- return age < AUTH_MAX_AGE_MS ? path : void 0;
344319
- }
344320
344971
  function buildHeaders(authToken, projectId) {
344321
344972
  return {
344322
344973
  "Content-Type": "application/json",
@@ -344365,7 +345016,13 @@ async function claimRun(serverUrl, headers, runId) {
344365
345016
  if (res.status === 429) return false;
344366
345017
  if (!res.ok) throw new Error(`Claim failed: ${res.status}`);
344367
345018
  const body = await res.json();
344368
- return body.claimed;
345019
+ if (!body.claimed) {
345020
+ throw new Error("Claim response did not confirm ownership");
345021
+ }
345022
+ if (typeof body.claimEpoch !== "number" || !Number.isSafeInteger(body.claimEpoch) || body.claimEpoch < 0) {
345023
+ throw new Error("Claim response did not include a valid ownership epoch");
345024
+ }
345025
+ return body.claimEpoch;
344369
345026
  }
344370
345027
  async function postResult(serverUrl, headers, runId, result) {
344371
345028
  const MAX_ATTEMPTS = 3;
@@ -344533,12 +345190,12 @@ async function postCaptureResult(serverUrl, headers, runId, result) {
344533
345190
  });
344534
345191
  if (!res.ok) throw new Error(`Failed to post capture result: ${res.status}`);
344535
345192
  }
344536
- async function postCredentialRotation(serverUrl, headers, runId, body) {
345193
+ async function postCredentialRotation(serverUrl, headers, runId, body, claimEpoch) {
344537
345194
  try {
344538
345195
  const res = await fetch(`${serverUrl}/api/runner/runs/${runId}/credential`, {
344539
345196
  method: "POST",
344540
345197
  headers,
344541
- body: JSON.stringify(body),
345198
+ body: JSON.stringify({ ...body, ...claimEpoch !== void 0 ? { claimEpoch } : {} }),
344542
345199
  signal: AbortSignal.timeout(3e4)
344543
345200
  });
344544
345201
  if (!res.ok) {
@@ -344875,6 +345532,12 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
344875
345532
  liveBrowserHandle,
344876
345533
  onLiveBrowserSnapshot: requestLiveBrowserHeartbeat
344877
345534
  });
345535
+ const callbackContext = {
345536
+ runId: run2.runId,
345537
+ serverUrl: opts.serverUrl,
345538
+ authHeader: headers.Authorization,
345539
+ claimEpoch: run2.claimEpoch
345540
+ };
344878
345541
  const startTime = Date.now();
344879
345542
  try {
344880
345543
  if (mobileLeaseError) {
@@ -344904,7 +345567,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
344904
345567
  log.fail(`PRE-FLIGHT FAILED - ${preflightError}`);
344905
345568
  return;
344906
345569
  }
344907
- const preflight = await (0, import_runner_core12.resolveBaseUrlPreflight)(run2.baseUrl);
345570
+ const preflight = await (0, import_runner_core13.resolveBaseUrlPreflight)(run2.baseUrl);
344908
345571
  if (preflight.error) {
344909
345572
  await postResult(opts.serverUrl, headers, run2.runId, { status: "ERROR", error: preflight.error });
344910
345573
  log.fail(`PRE-FLIGHT FAILED - ${preflight.error}`);
@@ -344921,31 +345584,72 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
344921
345584
  log.fail("ERROR - No auth setup code");
344922
345585
  return;
344923
345586
  }
344924
- const runAuthPath = authStatePath(run2.projectId || opts.projectId || "default");
344925
- log.info(`Auth setup: saving session state to ${runAuthPath}`);
344926
- const result2 = await runNativeWithLiveBrowser({
344927
- playwrightCode: run2.playwrightCode,
345587
+ const sharedAuthEnabled = sharedAuthInjectEnabled();
345588
+ if ((0, import_runner_core12.shouldSkipDisabledAutomaticAuthSetup)(run2.authForceRun, run2.authVerify, sharedAuthEnabled)) {
345589
+ const duration2 = Date.now() - startTime;
345590
+ await postResult(opts.serverUrl, headers, run2.runId, {
345591
+ status: "PASSED",
345592
+ logs: "Automatic auth prewarm skipped: shared session reuse is disabled.",
345593
+ duration: duration2
345594
+ });
345595
+ log.info(`Auth prewarm skipped (${duration2}ms) - SHARED_AUTH_INJECT=false`);
345596
+ return;
345597
+ }
345598
+ const authPaths2 = sharedAuthEnabled ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
345599
+ projectId: run2.projectId || opts.projectId || "default",
345600
+ serverUrl: opts.serverUrl,
344928
345601
  baseUrl: run2.baseUrl,
344929
- testName: "auth-setup",
344930
- testVariables: { ...run2.testVariables, AUTH_FILE: runAuthPath },
344931
- validateContext: {
344932
- runId: run2.runId,
344933
- serverUrl: opts.serverUrl,
344934
- authHeader: headers.Authorization
345602
+ testVariables: run2.testVariables,
345603
+ authToken: opts.authToken
345604
+ }, run2.runId) : void 0;
345605
+ const releaseAuthLock2 = authPaths2 ? await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths2.lockKey, authPaths2.lockPath) : void 0;
345606
+ try {
345607
+ if (authPaths2 && (0, import_runner_core12.shouldSkipWarmAuthSetup)(
345608
+ run2.authForceRun,
345609
+ sharedAuthEnabled,
345610
+ !!(0, import_runner_core12.getValidLocalAuthState)(authPaths2.cachePath)
345611
+ )) {
345612
+ const duration3 = Date.now() - startTime;
345613
+ await postResult(opts.serverUrl, headers, run2.runId, {
345614
+ status: "PASSED",
345615
+ logs: "Auth setup skipped: the credential-bound local session is already warm.",
345616
+ duration: duration3
345617
+ });
345618
+ log.ok(`Auth setup skipped (${duration3}ms) - local session already warm`);
345619
+ return;
344935
345620
  }
344936
- });
344937
- const duration2 = Date.now() - startTime;
344938
- await postResult(opts.serverUrl, headers, run2.runId, {
344939
- status: result2.passed ? "PASSED" : "FAILED",
344940
- apiCalls: result2.apiCalls,
344941
- logs: result2.logs,
344942
- error: result2.error,
344943
- duration: duration2
344944
- });
344945
- log[result2.passed ? "ok" : "fail"](
344946
- `Auth setup ${result2.passed ? "PASSED" : "FAILED"} (${duration2}ms)${result2.error ? ` - ${result2.error}` : ""}`
344947
- );
344948
- return;
345621
+ 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");
345622
+ const capturePath = sharedAuthEnabled && !run2.authVerify ? authPaths2?.capturePath : void 0;
345623
+ const result2 = await runNativeWithLiveBrowser({
345624
+ playwrightCode: run2.playwrightCode,
345625
+ baseUrl: run2.baseUrl,
345626
+ testName: "auth-setup",
345627
+ testVariables: run2.testVariables,
345628
+ authStateCapturePath: capturePath,
345629
+ validateContext: callbackContext
345630
+ });
345631
+ const captured = !!capturePath && !!authPaths2 && result2.passed && !result2.suspiciousPass && (0, import_runner_core12.promoteLocalAuthStateCapture)(
345632
+ capturePath,
345633
+ authPaths2.cachePath,
345634
+ run2.authForceRun === true,
345635
+ releaseAuthLock2
345636
+ );
345637
+ const duration2 = Date.now() - startTime;
345638
+ await postResult(opts.serverUrl, headers, run2.runId, {
345639
+ status: result2.passed ? "PASSED" : "FAILED",
345640
+ apiCalls: result2.apiCalls,
345641
+ logs: result2.logs,
345642
+ error: result2.error,
345643
+ duration: duration2
345644
+ });
345645
+ log[result2.passed ? "ok" : "fail"](
345646
+ `Auth setup ${result2.passed ? "PASSED" : "FAILED"} (${duration2}ms)${captured ? " - session cached" : ""}${result2.error ? ` - ${result2.error}` : ""}`
345647
+ );
345648
+ return;
345649
+ } finally {
345650
+ if (authPaths2?.capturePath) (0, import_runner_core12.discardLocalAuthStateCapture)(authPaths2.capturePath);
345651
+ releaseAuthLock2?.();
345652
+ }
344949
345653
  }
344950
345654
  if (runMode === "batch-heal") {
344951
345655
  const tests = run2.batchHealTests ?? [];
@@ -344964,7 +345668,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
344964
345668
  const batchResult = await (0, import_runner_core10.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
344965
345669
  testVariables: run2.testVariables,
344966
345670
  runNativeTest: runNativeWithLiveBrowser,
344967
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
345671
+ validateContext: callbackContext,
344968
345672
  onLog: healLogStream.onLog
344969
345673
  });
344970
345674
  await healLogStream.flushNow();
@@ -345000,6 +345704,15 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
345000
345704
  );
345001
345705
  const describeKind = isSerialDescribe ? "serial" : "non-serial";
345002
345706
  log.info(`API test heal - ${subTestCount} sub-test(s) [${describeKind}], ${failingSubTestCount} budgeted, ${MAX_API_HEAL_ATTEMPTS} attempts`);
345707
+ const apiHealAuthPaths = shouldUseAuthState(run2.tags, run2.playwrightCode) ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
345708
+ projectId: run2.projectId || opts.projectId,
345709
+ serverUrl: opts.serverUrl,
345710
+ baseUrl: run2.baseUrl,
345711
+ testVariables: run2.testVariables,
345712
+ authToken: opts.authToken
345713
+ }, run2.runId) : void 0;
345714
+ const apiHealStorageStatePath = apiHealAuthPaths ? (0, import_runner_core12.getValidLocalAuthState)(apiHealAuthPaths.cachePath) : void 0;
345715
+ if (apiHealStorageStatePath) log.info("API heal verification is using saved auth state");
345003
345716
  const stickyAppBugs = /* @__PURE__ */ new Map();
345004
345717
  const lastChangeByTitle = /* @__PURE__ */ new Map();
345005
345718
  let currentError = run2.lastFailureError ?? "Test failed";
@@ -345065,7 +345778,8 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
345065
345778
  playwrightCode: lastHealedCode,
345066
345779
  baseUrl: run2.baseUrl,
345067
345780
  testVariables: run2.testVariables,
345068
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization }
345781
+ storageStatePath: apiHealStorageStatePath,
345782
+ validateContext: callbackContext
345069
345783
  });
345070
345784
  if (finalVerifyResult.passed) {
345071
345785
  log.ok(` Attempt ${attempt} terminal verification PASSED (${bugCount} app_bug, ${passCount} pass)`);
@@ -345085,7 +345799,9 @@ ${finalVerifyResult.logs ?? ""}`;
345085
345799
  finalVerifyResult = await runNativeWithLiveBrowser({
345086
345800
  playwrightCode: lastHealedCode,
345087
345801
  baseUrl: run2.baseUrl,
345088
- testVariables: run2.testVariables
345802
+ testVariables: run2.testVariables,
345803
+ storageStatePath: apiHealStorageStatePath,
345804
+ validateContext: callbackContext
345089
345805
  });
345090
345806
  const stepResults = finalVerifyResult.stepResults ?? [];
345091
345807
  const nonAppBugFails = stepResults.filter(
@@ -345135,7 +345851,8 @@ ${finalVerifyResult.logs ?? ""}`;
345135
345851
  playwrightCode: lastHealedCode,
345136
345852
  baseUrl: run2.baseUrl,
345137
345853
  testVariables: run2.testVariables,
345138
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization }
345854
+ storageStatePath: apiHealStorageStatePath,
345855
+ validateContext: callbackContext
345139
345856
  });
345140
345857
  if (finalVerifyResult.passed) {
345141
345858
  log.ok(` Attempt ${attempt} verification PASSED`);
@@ -345188,7 +345905,7 @@ ${finalVerifyResult.logs ?? ""}`;
345188
345905
  if (classifyErrorMessage(composedError ?? null) === "config_issue") {
345189
345906
  apiHealStatus = "ERROR";
345190
345907
  } else {
345191
- const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
345908
+ const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
345192
345909
  testName: run2.testName ?? run2.testCaseId ?? void 0,
345193
345910
  tags: run2.tags ?? [],
345194
345911
  error: composedError ?? finalVerifyResult?.error ?? null,
@@ -345285,8 +346002,8 @@ ${finalVerifyResult.logs ?? ""}`;
345285
346002
  lastHealAttempt: run2.lastHealAttempt ?? null,
345286
346003
  lastHealedCode: run2.lastHealedCode ?? null,
345287
346004
  failureScreenshot: run2.failureScreenshot ?? null,
345288
- onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }),
345289
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
346005
+ onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }, run2.claimEpoch),
346006
+ validateContext: callbackContext,
345290
346007
  liveBrowserHandle,
345291
346008
  runNativeTest: runNativeWithLiveBrowser
345292
346009
  };
@@ -345328,7 +346045,7 @@ ${finalVerifyResult.logs ?? ""}`;
345328
346045
  let healStatus = result2.passed ? "PASSED" : isConfigIssue ? "ERROR" : "FAILED";
345329
346046
  let healError = result2.error;
345330
346047
  if (!result2.passed && !isConfigIssue) {
345331
- const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
346048
+ const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
345332
346049
  testName: run2.testName ?? run2.testCaseId ?? void 0,
345333
346050
  tags: run2.tags ?? [],
345334
346051
  error: result2.error,
@@ -345399,7 +346116,8 @@ ${finalVerifyResult.logs ?? ""}`;
345399
346116
  // Allow generated `persistCredential(...)` calls inside the suite to
345400
346117
  // POST back to the server with the runner's bearer token.
345401
346118
  validateServerUrl: opts.serverUrl,
345402
- validateRunnerAuthHeader: headers.Authorization
346119
+ validateRunnerAuthHeader: headers.Authorization,
346120
+ claimEpoch: run2.claimEpoch
345403
346121
  });
345404
346122
  const overall = result2.error && result2.testResults.length === 0 ? "ERROR" : result2.passed ? "PASSED" : "FAILED";
345405
346123
  const rr = await fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/regression-result`, {
@@ -345454,7 +346172,7 @@ ${finalVerifyResult.logs ?? ""}`;
345454
346172
  playwrightCode: healedCode,
345455
346173
  baseUrl: run2.baseUrl,
345456
346174
  testVariables: run2.testVariables,
345457
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization }
346175
+ validateContext: callbackContext
345458
346176
  });
345459
346177
  results.push({
345460
346178
  testCaseId: failedTest.testCaseId,
@@ -345517,8 +346235,8 @@ ${finalVerifyResult.logs ?? ""}`;
345517
346235
  onAICall: healAudit,
345518
346236
  modelOverride: run2.healerModel,
345519
346237
  consecutiveFails: failedTest.consecutiveFails,
345520
- onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }),
345521
- validateContext: { runId: run2.runId, serverUrl: opts.serverUrl, authHeader: headers.Authorization },
346238
+ onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-healer" }, run2.claimEpoch),
346239
+ validateContext: callbackContext,
345522
346240
  liveBrowserHandle,
345523
346241
  runNativeTest: runNativeWithLiveBrowser
345524
346242
  };
@@ -345723,6 +346441,13 @@ ${finalVerifyResult.logs ?? ""}`;
345723
346441
  }
345724
346442
  return;
345725
346443
  }
346444
+ ctx.onCredentialUpdate = async (input) => postCredentialRotation(
346445
+ opts.serverUrl,
346446
+ headers,
346447
+ run2.runId,
346448
+ { ...input, actor: "ai-explorer" },
346449
+ run2.claimEpoch
346450
+ );
345726
346451
  log.info(`Discovery (${ctx.mode}) on ${ctx.baseUrl}`);
345727
346452
  const { push: pushAudit, drain: drainAudit } = createAuditFlusher(
345728
346453
  `${opts.serverUrl}/api/runner/runs/${run2.runId}/ai-audit-batch`,
@@ -346425,7 +347150,7 @@ ${finalVerifyResult.logs ?? ""}`;
346425
347150
  featureDescription: ctx.featureDescription,
346426
347151
  featurePages: ctx.featurePages,
346427
347152
  appBrief: ctx.appBrief,
346428
- onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-capture" })
347153
+ onCredentialUpdate: async (input) => postCredentialRotation(opts.serverUrl, headers, run2.runId, { ...input, actor: "ai-capture" }, run2.claimEpoch)
346429
347154
  });
346430
347155
  if (logBuffer.length > 0) {
346431
347156
  postCaptureLog(opts.serverUrl, headers, run2.runId, ctx.jobId, truncateTextTail(logBuffer.join("\n"), DISCOVERY_LOG_POST_MAX_CHARS, "capture log"));
@@ -346670,7 +347395,7 @@ ${finalVerifyResult.logs ?? ""}`;
346670
347395
  }
346671
347396
  return verify;
346672
347397
  };
346673
- return await (0, import_runner_core13.executeMobileLiveHeal)({
347398
+ return await (0, import_runner_core14.executeMobileLiveHeal)({
346674
347399
  steps: a.steps,
346675
347400
  target: a.target,
346676
347401
  testVariables: a.testVariables,
@@ -346687,7 +347412,7 @@ ${finalVerifyResult.logs ?? ""}`;
346687
347412
  }
346688
347413
  };
346689
347414
  log.info("\u{1FA7A} Mobile AI heal \u2014 Phase 1 (script) \u2192 Phase 2 (live device, lazy)...");
346690
- const heal = await (0, import_runner_core13.executeMobileTestWithHealing)(
347415
+ const heal = await (0, import_runner_core14.executeMobileTestWithHealing)(
346691
347416
  mobileSteps,
346692
347417
  healTarget,
346693
347418
  {
@@ -346700,7 +347425,7 @@ ${finalVerifyResult.logs ?? ""}`;
346700
347425
  healerOptions
346701
347426
  );
346702
347427
  const finalRes = nativeVerify.last;
346703
- const outcome = (0, import_runner_core13.computeMobileHealOutcome)({
347428
+ const outcome = (0, import_runner_core14.computeMobileHealOutcome)({
346704
347429
  heal,
346705
347430
  finalError: finalRes?.error ?? null,
346706
347431
  reproduceError,
@@ -346846,26 +347571,77 @@ ${result2.logs}`);
346846
347571
  }
346847
347572
  return;
346848
347573
  }
346849
- const storageStatePath = shouldUseAuthState(run2.tags, run2.playwrightCode) ? getValidAuthState(run2.projectId || opts.projectId) : void 0;
346850
- if (storageStatePath) {
346851
- log.info(`Using auth state for project ${run2.projectId || opts.projectId}`);
346852
- }
346853
- const result = await runNativeWithLiveBrowser({
346854
- playwrightCode: run2.playwrightCode,
347574
+ const projectId = run2.projectId || opts.projectId;
347575
+ const authStateExpected = shouldUseAuthState(run2.tags, run2.playwrightCode);
347576
+ const captureAuth = shouldCaptureAuthState(run2.tags, run2.playwrightCode);
347577
+ const authPaths = authStateExpected || captureAuth ? (0, import_runner_core12.localAuthStatePaths)(getRunsDir(), {
347578
+ projectId,
347579
+ serverUrl: opts.serverUrl,
346855
347580
  baseUrl: run2.baseUrl,
346856
347581
  testVariables: run2.testVariables,
346857
- storageStatePath,
346858
- validateContext: {
346859
- runId: run2.runId,
346860
- serverUrl: opts.serverUrl,
346861
- authHeader: headers.Authorization
347582
+ authToken: opts.authToken
347583
+ }, run2.runId) : void 0;
347584
+ let releaseAuthLock;
347585
+ let storageStatePath;
347586
+ if (authPaths && authStateExpected) {
347587
+ releaseAuthLock = await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths.lockKey, authPaths.lockPath);
347588
+ storageStatePath = (0, import_runner_core12.getValidLocalAuthState)(authPaths.cachePath);
347589
+ if (storageStatePath || !captureAuth) {
347590
+ releaseAuthLock();
347591
+ releaseAuthLock = void 0;
347592
+ }
347593
+ if (!storageStatePath && !captureAuth && sharedAuthInjectEnabled()) {
347594
+ log.info(`Waiting for the auth producer to warm project ${projectId}`);
347595
+ storageStatePath = await (0, import_runner_core12.waitForLocalAuthStateProducer)(
347596
+ authPaths.cachePath,
347597
+ authPaths.lockKey,
347598
+ 2e3,
347599
+ void 0,
347600
+ 100,
347601
+ authPaths.lockPath
347602
+ );
346862
347603
  }
346863
- });
347604
+ }
347605
+ if (storageStatePath) {
347606
+ log.info(`Using auth state for project ${projectId}`);
347607
+ }
347608
+ const runAuthPath = captureAuth ? authPaths?.capturePath : void 0;
347609
+ const result = await (async () => {
347610
+ try {
347611
+ return await runNativeWithLiveBrowser({
347612
+ playwrightCode: run2.playwrightCode,
347613
+ baseUrl: run2.baseUrl,
347614
+ testVariables: run2.testVariables,
347615
+ authStateCapturePath: runAuthPath,
347616
+ storageStatePath,
347617
+ validateContext: callbackContext
347618
+ });
347619
+ } finally {
347620
+ if (runAuthPath && authPaths) {
347621
+ if ((0, import_runner_core12.getValidLocalAuthState)(runAuthPath)) {
347622
+ const releasePromotionLock = releaseAuthLock ? void 0 : await (0, import_runner_core12.acquireLocalAuthStateLock)(authPaths.lockKey, authPaths.lockPath);
347623
+ try {
347624
+ const captured = (0, import_runner_core12.promoteLocalAuthStateCapture)(
347625
+ runAuthPath,
347626
+ authPaths.cachePath,
347627
+ !!storageStatePath,
347628
+ releaseAuthLock ?? releasePromotionLock
347629
+ );
347630
+ if (captured) log.info(`Captured auth state for project ${projectId}`);
347631
+ } finally {
347632
+ releasePromotionLock?.();
347633
+ }
347634
+ }
347635
+ (0, import_runner_core12.discardLocalAuthStateCapture)(runAuthPath);
347636
+ }
347637
+ releaseAuthLock?.();
347638
+ }
347639
+ })();
346864
347640
  const duration = Date.now() - startTime;
346865
347641
  let runStatus = result.passed ? "PASSED" : "FAILED";
346866
347642
  let runError = result.error;
346867
347643
  if (!result.passed) {
346868
- const rlDetection = (0, import_runner_core12.detectRateLimitBlock)({
347644
+ const rlDetection = (0, import_runner_core13.detectRateLimitBlock)({
346869
347645
  testName: run2.testName ?? run2.testCaseId ?? void 0,
346870
347646
  tags: run2.tags ?? [],
346871
347647
  error: result.error,
@@ -346926,6 +347702,12 @@ async function startRunner(opts) {
346926
347702
  process.env.AUTH_TOKEN = authToken;
346927
347703
  process.env.RUNNER_TOKEN = authToken;
346928
347704
  if (projectId) process.env.PROJECT_ID = projectId;
347705
+ (0, import_runner_core12.cleanupExpiredLocalAuthStateFiles)(getRunsDir());
347706
+ const authCleanupTimer = setInterval(
347707
+ () => (0, import_runner_core12.cleanupExpiredLocalAuthStateFiles)(getRunsDir()),
347708
+ import_runner_core12.LOCAL_AUTH_STATE_CLEANUP_INTERVAL_MS
347709
+ );
347710
+ authCleanupTimer.unref?.();
346929
347711
  const headers = buildHeaders(authToken, projectId);
346930
347712
  console.log("");
346931
347713
  console.log(`${C2.bold}${C2.cyan}validate.qa Local Runner${C2.reset}`);
@@ -347034,7 +347816,7 @@ async function startRunner(opts) {
347034
347816
  capture: envInt("MAX_CAPTURE", DEFAULT_TYPE_LIMITS.capture),
347035
347817
  run: envInt("MAX_RUN", DEFAULT_TYPE_LIMITS.run)
347036
347818
  };
347037
- const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core12.createResourceGovernorOptions)({
347819
+ const RESOURCE_GOVERNOR_OPTIONS = (0, import_runner_core13.createResourceGovernorOptions)({
347038
347820
  isCloud: IS_CLOUD,
347039
347821
  maxConcurrent: MAX_CONCURRENT,
347040
347822
  typeLimits: TYPE_LIMITS
@@ -347202,8 +347984,8 @@ async function startRunner(opts) {
347202
347984
  android: devices.some((d) => d.platform === "ANDROID" && d.isAvailable && d.state === "BOOTED")
347203
347985
  } : { web: true, ios: false, android: false };
347204
347986
  const bridge = enableMobile ? getBridgeState() : void 0;
347205
- const resourceStatus = (0, import_runner_core12.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
347206
- const processInventory = await (0, import_runner_core12.getBrowserProcessInventory)();
347987
+ const resourceStatus = (0, import_runner_core13.getResourceGovernorStatus)(RESOURCE_GOVERNOR_OPTIONS, Object.fromEntries(activeByType));
347988
+ const processInventory = await (0, import_runner_core13.getBrowserProcessInventory)();
347207
347989
  lastInventoryPids = new Set(processInventory.processes.map((p) => p.pid));
347208
347990
  lastInventoryKinds.clear();
347209
347991
  for (const p of processInventory.processes) lastInventoryKinds.set(p.pid, p.kind);
@@ -347233,7 +348015,7 @@ async function startRunner(opts) {
347233
348015
  resourceSnapshot: resourceStatus.snapshot,
347234
348016
  resourceGovernor: resourceStatus.governor,
347235
348017
  processInventory,
347236
- aiQueue: (0, import_runner_core12.getRunnerAIQueueStats)(),
348018
+ aiQueue: (0, import_runner_core13.getRunnerAIQueueStats)(),
347237
348019
  liveBrowsers: import_runner_core11.liveBrowserRegistry.snapshotForHeartbeat(),
347238
348020
  // Pre-flight checks (ANDROID_HOME, Xcode, Appium drivers, adb, etc).
347239
348021
  // Cached & cheap — see getCachedDoctor above.
@@ -347344,7 +348126,7 @@ async function startRunner(opts) {
347344
348126
  let pidsWatchdogWarned = false;
347345
348127
  const pidsWatchdog = setInterval(() => {
347346
348128
  if (shuttingDown) return;
347347
- const snapshot = (0, import_runner_core12.getResourceSnapshot)();
348129
+ const snapshot = (0, import_runner_core13.getResourceSnapshot)();
347348
348130
  const pct = snapshot.pidsPct;
347349
348131
  if (pct == null || !Number.isFinite(pct)) return;
347350
348132
  if (pct >= PIDS_RECYCLE_PCT) {
@@ -347384,7 +348166,7 @@ async function startRunner(opts) {
347384
348166
  log.dim(` Deferred ${run2.runId} (project ${runProject} at capacity ${getActiveForProject(runProject)}/${projectLimit})`);
347385
348167
  continue;
347386
348168
  }
347387
- const admission = (0, import_runner_core12.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
348169
+ const admission = (0, import_runner_core13.evaluateRunAdmission)(RESOURCE_GOVERNOR_OPTIONS, {
347388
348170
  mode: runMode,
347389
348171
  activeRuns,
347390
348172
  activeByType: Object.fromEntries(activeByType),
@@ -347462,9 +348244,9 @@ async function startRunner(opts) {
347462
348244
  leasedDeviceId = freeDeviceId;
347463
348245
  leasedDeviceIds.set(freeDeviceId, run2.runId);
347464
348246
  }
347465
- let claimed = false;
348247
+ let claimEpoch = false;
347466
348248
  try {
347467
- claimed = await claimRun(serverUrl, headers, run2.runId);
348249
+ claimEpoch = await claimRun(serverUrl, headers, run2.runId);
347468
348250
  } catch (err) {
347469
348251
  if (leasedDeviceId) {
347470
348252
  leasedDeviceIds.delete(leasedDeviceId);
@@ -347472,7 +348254,7 @@ async function startRunner(opts) {
347472
348254
  }
347473
348255
  throw err;
347474
348256
  }
347475
- if (!claimed) {
348257
+ if (claimEpoch === false) {
347476
348258
  log.dim(` Skipped ${run2.runId} (already claimed)`);
347477
348259
  if (leasedDeviceId) {
347478
348260
  leasedDeviceIds.delete(leasedDeviceId);
@@ -347480,6 +348262,7 @@ async function startRunner(opts) {
347480
348262
  }
347481
348263
  continue;
347482
348264
  }
348265
+ run2.claimEpoch = claimEpoch;
347483
348266
  activeRuns++;
347484
348267
  incrementType(runMode);
347485
348268
  incrementProject(runProject);