@validate.qa/runner 1.0.102 → 1.0.104
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.
- package/README.md +7 -2
- package/dist/{chunk-E2VEMBQQ.mjs → chunk-6N4QCCEH.mjs} +2 -2
- package/dist/{chunk-ZZYBRIBT.mjs → chunk-EBWZ7NGU.mjs} +11 -8
- package/dist/{chunk-3JM6XTSZ.mjs → chunk-EKY6CN4Y.mjs} +660 -123
- package/dist/{chunk-DN3O6EYT.mjs → chunk-MEE4DXRX.mjs} +19834 -3064
- package/dist/{chunk-CBVHPYVJ.mjs → chunk-OXDU6ARE.mjs} +86 -7
- package/dist/{chunk-AAUHXGDS.mjs → chunk-PUMRAFJS.mjs} +37 -13
- package/dist/{chunk-4WLP3RWN.mjs → chunk-TGVKABFN.mjs} +45 -3
- package/dist/{chunk-5DFBEL72.mjs → chunk-TS4OUJVT.mjs} +1 -1
- package/dist/{chunk-BXLEPT4M.mjs → chunk-UR7KDOTP.mjs} +32 -8
- package/dist/{chunk-E2RRAE27.mjs → chunk-UZ67UBXH.mjs} +14 -3
- package/dist/{chunk-HJ6FKHMM.mjs → chunk-VWE3SQ2Z.mjs} +1 -1
- package/dist/{chunk-PWFWMVZU.mjs → chunk-XXZZMG6Q.mjs} +15 -3
- package/dist/cli.js +35519 -17101
- package/dist/cli.mjs +1171 -251
- package/dist/credential-mcp-server.js +750 -133
- package/dist/credential-mcp-server.mjs +3 -3
- package/dist/discovery-mcp-server.js +714 -132
- package/dist/discovery-mcp-server.mjs +4 -4
- package/dist/dist-BXHLIG5H.mjs +17 -0
- package/dist/inbox-mcp-server.js +743 -129
- package/dist/inbox-mcp-server.mjs +3 -3
- package/dist/route-seed-worker.js +672 -125
- package/dist/route-seed-worker.mjs +3 -3
- package/dist/run-write-guard-preload.js +714 -126
- package/dist/run-write-guard-preload.mjs +4 -4
- package/dist/run-write-guard.js +714 -126
- package/dist/run-write-guard.mjs +3 -3
- package/dist/tar-extract-child.js +38 -14
- package/dist/tar-extract-child.mjs +2 -2
- package/package.json +3 -2
- package/dist/dist-52M5RDZ6.mjs +0 -17
package/dist/cli.mjs
CHANGED
|
@@ -33,23 +33,23 @@ import {
|
|
|
33
33
|
require_resource_governor,
|
|
34
34
|
require_result_api_call_bodies,
|
|
35
35
|
require_result_shed
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-MEE4DXRX.mjs";
|
|
37
37
|
import {
|
|
38
38
|
require_tar_isolated
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-PUMRAFJS.mjs";
|
|
40
40
|
import "./chunk-EXRFHATN.mjs";
|
|
41
41
|
import {
|
|
42
42
|
require_browser_config,
|
|
43
43
|
require_cloud_runner
|
|
44
|
-
} from "./chunk-
|
|
45
|
-
import "./chunk-
|
|
46
|
-
import "./chunk-
|
|
47
|
-
import "./chunk-
|
|
48
|
-
import "./chunk-
|
|
49
|
-
import "./chunk-
|
|
50
|
-
import "./chunk-
|
|
51
|
-
import "./chunk-
|
|
52
|
-
import "./chunk-
|
|
44
|
+
} from "./chunk-EBWZ7NGU.mjs";
|
|
45
|
+
import "./chunk-UR7KDOTP.mjs";
|
|
46
|
+
import "./chunk-TS4OUJVT.mjs";
|
|
47
|
+
import "./chunk-OXDU6ARE.mjs";
|
|
48
|
+
import "./chunk-VWE3SQ2Z.mjs";
|
|
49
|
+
import "./chunk-XXZZMG6Q.mjs";
|
|
50
|
+
import "./chunk-6N4QCCEH.mjs";
|
|
51
|
+
import "./chunk-TGVKABFN.mjs";
|
|
52
|
+
import "./chunk-UZ67UBXH.mjs";
|
|
53
53
|
import {
|
|
54
54
|
LIVE_FRAME_IDLE_PUSH_INTERVAL_MS,
|
|
55
55
|
MOBILE_COLD_BOOT_SETTLE_MS,
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
MOBILE_MJPEG_SETTINGS,
|
|
60
60
|
MOBILE_POST_ACTION_SETTLE_MS,
|
|
61
61
|
MOBILE_SCREENSHOT_INTERVAL_MS,
|
|
62
|
+
blockedRunLogLine,
|
|
62
63
|
buildAppiumCode,
|
|
63
64
|
clampMobileSwipeDistance,
|
|
64
65
|
classifyErrorMessage,
|
|
@@ -77,6 +78,7 @@ import {
|
|
|
77
78
|
isValidMobileAppId,
|
|
78
79
|
mobilePlatformFromMedium,
|
|
79
80
|
parseBoolFlag,
|
|
81
|
+
registrableDomain,
|
|
80
82
|
repositoryCliLabel,
|
|
81
83
|
resolveCrossOriginConfigOverride,
|
|
82
84
|
resolveMobileStepValuePlaceholders,
|
|
@@ -84,7 +86,7 @@ import {
|
|
|
84
86
|
stepAppliesToPlatform,
|
|
85
87
|
stepsMissingPlatformLocators,
|
|
86
88
|
stepsNeedingReviewForPlatform
|
|
87
|
-
} from "./chunk-
|
|
89
|
+
} from "./chunk-EKY6CN4Y.mjs";
|
|
88
90
|
import {
|
|
89
91
|
__commonJS,
|
|
90
92
|
__require,
|
|
@@ -215,13 +217,41 @@ var require_tls_cert_probe = __commonJS({
|
|
|
215
217
|
});
|
|
216
218
|
});
|
|
217
219
|
}
|
|
220
|
+
var MAX_SPEC_HOSTS = 3;
|
|
221
|
+
var SPEC_GOTO_HTTPS = /\.goto\(\s*['"`](https:\/\/[^\s/?#'"`\\$]{1,300})(?=[/?#'"`])/gi;
|
|
222
|
+
function httpsOrigin(raw) {
|
|
223
|
+
try {
|
|
224
|
+
const url = new URL(raw);
|
|
225
|
+
return url.protocol === "https:" && url.hostname ? `https://${url.host}/` : null;
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function specNavigationOrigins(specCode, baseUrl) {
|
|
231
|
+
const origins = [];
|
|
232
|
+
if (!specCode)
|
|
233
|
+
return origins;
|
|
234
|
+
const base = httpsOrigin(baseUrl);
|
|
235
|
+
for (const match of specCode.matchAll(SPEC_GOTO_HTTPS)) {
|
|
236
|
+
const origin = httpsOrigin(`${match[1]}/`);
|
|
237
|
+
if (!origin || origin === base || origins.includes(origin))
|
|
238
|
+
continue;
|
|
239
|
+
origins.push(origin);
|
|
240
|
+
if (origins.length === MAX_SPEC_HOSTS)
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
return origins;
|
|
244
|
+
}
|
|
218
245
|
function tlsCertFindingLine(finding) {
|
|
219
246
|
return `\u26A0\uFE0F TLS certificate: ${finding.host} presented a certificate that strict TLS rejects (${finding.code}: ${finding.detail}). The run went ahead because test browsers ignore HTTPS errors, so this is not a failure. A visitor's browser may show a certificate warning: this check does not fetch a missing intermediate certificate and does not use the operating system's trust store (a private CA the visitor's machine trusts), so a browser can accept a certificate it rejects.`;
|
|
220
247
|
}
|
|
221
248
|
async function targetCertificateNote2(baseUrl, opts = {}) {
|
|
222
249
|
try {
|
|
223
|
-
const
|
|
224
|
-
|
|
250
|
+
const { specCode, ...probeOpts } = opts;
|
|
251
|
+
const targets = [baseUrl, ...specNavigationOrigins(specCode, baseUrl)];
|
|
252
|
+
const findings = await Promise.all(targets.map((target) => probeTargetCertificate(target, probeOpts)));
|
|
253
|
+
const lines = findings.flatMap((finding) => finding ? [tlsCertFindingLine(finding)] : []);
|
|
254
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
225
255
|
} catch {
|
|
226
256
|
return null;
|
|
227
257
|
}
|
|
@@ -252,7 +282,7 @@ var require_package = __commonJS({
|
|
|
252
282
|
"package.json"(exports, module) {
|
|
253
283
|
module.exports = {
|
|
254
284
|
name: "@validate.qa/runner",
|
|
255
|
-
version: "1.0.
|
|
285
|
+
version: "1.0.104",
|
|
256
286
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
257
287
|
bin: {
|
|
258
288
|
"validate-runner": "dist/cli.js"
|
|
@@ -261,8 +291,9 @@ var require_package = __commonJS({
|
|
|
261
291
|
build: "tsup",
|
|
262
292
|
dev: "tsx src/cli.ts",
|
|
263
293
|
lint: "eslint .",
|
|
264
|
-
test: "node --import tsx --test src/supervisor-child-argv.test.ts src/audit-scrub.test.ts src/retry-on-429.test.ts src/version-check.test.ts src/poll-interval.test.ts src/node-version-notice.test.ts src/services/auth-state.test.ts src/run-no-longer-active.test.ts src/mode-abort-no-verdict.test.ts src/discovery-render-type.test.ts src/batch-heal-no-start.test.ts src/heartbeat-active-runs.test.ts src/result-post-structure-413.test.ts src/result-shed-routes-413.test.ts src/result-post-retry.test.ts src/result-media-stale-epoch.test.ts src/run-write-guard-bundle.test.ts src/spec-driver-bundle.test.ts src/mcp-servers-bundle.test.ts src/codex-files-mcp-server-bundle.test.ts src/claim-nonce-not-logged.test.ts src/sign-in-capture.test.ts src/run-status-verdicts.test.ts src/sign-in-failure.test.ts src/mobile-ui-path-succeeded.test.ts src/supervisor-policy.test.ts src/login-check.test.ts src/heal-post-lost-ack.test.ts src/ui-heal-result-video.test.ts src/heal-log-stream-chunks.test.ts src/shutdown-claim-window.test.ts src/result-post-redeploy-budget.test.ts src/shutdown-signals.test.ts",
|
|
294
|
+
test: "node --import tsx --test src/supervisor-child-argv.test.ts src/audit-scrub.test.ts src/retry-on-429.test.ts src/version-check.test.ts src/runner-upgrade-exit.test.ts src/poll-interval.test.ts src/node-version-notice.test.ts src/services/auth-state.test.ts src/run-no-longer-active.test.ts src/mode-abort-no-verdict.test.ts src/discovery-render-type.test.ts src/batch-heal-no-start.test.ts src/heartbeat-active-runs.test.ts src/result-post-structure-413.test.ts src/result-shed-routes-413.test.ts src/result-post-retry.test.ts src/result-media-stale-epoch.test.ts src/run-write-guard-bundle.test.ts src/run-write-guard-bundle-skip.test.ts src/spec-driver-bundle.test.ts src/mcp-servers-bundle.test.ts src/codex-files-mcp-server-bundle.test.ts src/claim-nonce-not-logged.test.ts src/sign-in-capture.test.ts src/run-status-verdicts.test.ts src/sign-in-failure.test.ts src/mobile-ui-path-succeeded.test.ts src/supervisor-policy.test.ts src/login-check.test.ts src/config-permissions.test.ts src/heal-post-lost-ack.test.ts src/ui-heal-result-video.test.ts src/heal-log-stream-chunks.test.ts src/shutdown-claim-window.test.ts src/result-post-redeploy-budget.test.ts src/shutdown-signals.test.ts src/sign-in-detect.test.ts src/no-credentials-help.test.ts src/resolve-credentials.test.ts src/sign-in-url.test.ts src/status-check.test.ts src/status-exit-code.test.ts src/services/doctor-web.test.ts src/browser-pool-failure.test.ts src/login-check-wrong-server.test.ts src/login-wrong-server-cli.test.ts src/start-wrong-server-cli.test.ts src/plaintext-server-warning.test.ts",
|
|
265
295
|
"test:mobile": "node --import tsx --test --test-concurrency=2 src/runner.mobile-ai-env.test.ts src/runner.mobile-heal-audit-wiring.test.ts src/mobile-device-queue.test.ts src/device-lease.test.ts src/runner-watchdog.test.ts src/result-payload-budget.test.ts src/result-media-upload.test.ts src/services/appium-*.test.ts src/services/*mobile*.test.ts src/services/mobile/*.test.ts",
|
|
296
|
+
"test:browser": "RUN_BROWSER_TESTS=1 PLAYWRIGHT_HEADLESS=true node --import tsx --test src/sign-in-prove.browser.ts",
|
|
266
297
|
"smoke:installed": "node scripts/installed-run-smoke.mjs",
|
|
267
298
|
"check:packed-mcp": "node scripts/packed-install-check.mjs",
|
|
268
299
|
prepublishOnly: "npm run build"
|
|
@@ -426,22 +457,46 @@ var cwdEnvIgnored = loadCwdEnv({
|
|
|
426
457
|
import { Command } from "commander";
|
|
427
458
|
|
|
428
459
|
// src/config.ts
|
|
429
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2,
|
|
460
|
+
import { chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
430
461
|
import { join as join3 } from "path";
|
|
431
462
|
import { homedir } from "os";
|
|
432
463
|
var CONFIG_DIR = join3(homedir(), ".validate.qa");
|
|
433
464
|
var CREDENTIALS_FILE = join3(CONFIG_DIR, "credentials.json");
|
|
434
|
-
function
|
|
435
|
-
|
|
436
|
-
|
|
465
|
+
function setMode(path, mode) {
|
|
466
|
+
try {
|
|
467
|
+
chmodSync(path, mode);
|
|
468
|
+
} catch {
|
|
437
469
|
}
|
|
438
470
|
}
|
|
471
|
+
function ensureConfigDir() {
|
|
472
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
473
|
+
setMode(CONFIG_DIR, 448);
|
|
474
|
+
}
|
|
439
475
|
function saveCredentials(creds) {
|
|
440
476
|
ensureConfigDir();
|
|
441
|
-
|
|
477
|
+
const tmp = `${CREDENTIALS_FILE}.${process.pid}.tmp`;
|
|
478
|
+
try {
|
|
479
|
+
writeFileSync(tmp, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
480
|
+
setMode(tmp, 384);
|
|
481
|
+
renameSync(tmp, CREDENTIALS_FILE);
|
|
482
|
+
} catch (err) {
|
|
483
|
+
try {
|
|
484
|
+
unlinkSync(tmp);
|
|
485
|
+
} catch {
|
|
486
|
+
}
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function tightenCredentialsMode() {
|
|
491
|
+
if (process.platform === "win32") return;
|
|
492
|
+
try {
|
|
493
|
+
if ((statSync(CREDENTIALS_FILE).mode & 63) !== 0) setMode(CREDENTIALS_FILE, 384);
|
|
494
|
+
} catch {
|
|
495
|
+
}
|
|
442
496
|
}
|
|
443
497
|
function loadCredentials() {
|
|
444
498
|
if (!existsSync2(CREDENTIALS_FILE)) return null;
|
|
499
|
+
tightenCredentialsMode();
|
|
445
500
|
try {
|
|
446
501
|
const data = readFileSync2(CREDENTIALS_FILE, "utf-8");
|
|
447
502
|
const parsed = JSON.parse(data);
|
|
@@ -473,9 +528,9 @@ var import_playwright_native = __toESM(require_playwright_native());
|
|
|
473
528
|
var import_runner_core = __toESM(require_dist());
|
|
474
529
|
|
|
475
530
|
// src/runner.ts
|
|
476
|
-
var import_runner_core7 = __toESM(require_dist());
|
|
477
531
|
var import_runner_core8 = __toESM(require_dist());
|
|
478
532
|
var import_runner_core9 = __toESM(require_dist());
|
|
533
|
+
var import_runner_core10 = __toESM(require_dist());
|
|
479
534
|
var import_framework_import_runner = __toESM(require_framework_import_runner());
|
|
480
535
|
var import_grok_repo_enrichment_runner = __toESM(require_grok_repo_enrichment_runner());
|
|
481
536
|
var import_repo_brief_execution = __toESM(require_repo_brief_execution());
|
|
@@ -485,7 +540,7 @@ var import_playwright_signal_handlers = __toESM(require_playwright_signal_handle
|
|
|
485
540
|
var import_cli_limit = __toESM(require_cli_limit());
|
|
486
541
|
var import_fetch_cli_subscription = __toESM(require_fetch_cli_subscription());
|
|
487
542
|
var import_codex_cli = __toESM(require_codex_cli());
|
|
488
|
-
var
|
|
543
|
+
var import_runner_core11 = __toESM(require_dist());
|
|
489
544
|
|
|
490
545
|
// src/services/regression-runner.ts
|
|
491
546
|
var import_regression_runner = __toESM(require_regression_runner());
|
|
@@ -564,7 +619,7 @@ function authRefusalAction(input) {
|
|
|
564
619
|
|
|
565
620
|
// src/version-check.ts
|
|
566
621
|
var RUNNER_NPM_PACKAGE = "@validate.qa/runner";
|
|
567
|
-
var
|
|
622
|
+
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
568
623
|
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
569
624
|
function parseSemver(raw) {
|
|
570
625
|
const cleaned = raw.trim().replace(/^v/i, "");
|
|
@@ -602,11 +657,41 @@ function shouldSkipVersionCheck() {
|
|
|
602
657
|
}
|
|
603
658
|
return null;
|
|
604
659
|
}
|
|
660
|
+
function npmConfigFromEnv(key, env) {
|
|
661
|
+
const want = `npm_config_${key}`.toLowerCase();
|
|
662
|
+
for (const [name, value] of Object.entries(env)) {
|
|
663
|
+
if (name.toLowerCase() === want && typeof value === "string" && value.trim() !== "") return value.trim();
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
function runnerRegistryUrl(env = process.env) {
|
|
668
|
+
const scope = RUNNER_NPM_PACKAGE.split("/")[0];
|
|
669
|
+
for (const raw of [npmConfigFromEnv(`${scope}:registry`, env), npmConfigFromEnv("registry", env)]) {
|
|
670
|
+
if (raw === null) continue;
|
|
671
|
+
let url;
|
|
672
|
+
try {
|
|
673
|
+
url = new URL(raw);
|
|
674
|
+
} catch {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") continue;
|
|
678
|
+
url.username = "";
|
|
679
|
+
url.password = "";
|
|
680
|
+
url.search = "";
|
|
681
|
+
url.hash = "";
|
|
682
|
+
if (!url.pathname.endsWith("/")) url.pathname += "/";
|
|
683
|
+
return url;
|
|
684
|
+
}
|
|
685
|
+
return new URL(DEFAULT_NPM_REGISTRY);
|
|
686
|
+
}
|
|
687
|
+
function runnerLatestUrl(env = process.env) {
|
|
688
|
+
return new URL(`${encodeURIComponent(RUNNER_NPM_PACKAGE)}/latest`, runnerRegistryUrl(env)).href;
|
|
689
|
+
}
|
|
605
690
|
async function fetchLatestRunnerVersion(timeoutMs = DEFAULT_TIMEOUT_MS, fetchImpl = fetch) {
|
|
606
691
|
const controller = new AbortController();
|
|
607
692
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
608
693
|
try {
|
|
609
|
-
const res = await fetchImpl(
|
|
694
|
+
const res = await fetchImpl(runnerLatestUrl(), {
|
|
610
695
|
headers: {
|
|
611
696
|
Accept: "application/json",
|
|
612
697
|
// Identify ourselves; helps npm operators + caching intermediaries.
|
|
@@ -634,7 +719,7 @@ async function checkRunnerVersion(currentVersion, opts = {}) {
|
|
|
634
719
|
outdated: false,
|
|
635
720
|
currentVersion,
|
|
636
721
|
latestVersion: null,
|
|
637
|
-
skippedReason:
|
|
722
|
+
skippedReason: `could not reach npm registry ${runnerRegistryUrl().host} (or response was invalid)`
|
|
638
723
|
};
|
|
639
724
|
}
|
|
640
725
|
if (!parseSemver(currentVersion) || !parseSemver(latestVersion)) {
|
|
@@ -672,6 +757,15 @@ function formatUpgradeInstructions(latestVersion) {
|
|
|
672
757
|
];
|
|
673
758
|
return lines.join("\n");
|
|
674
759
|
}
|
|
760
|
+
function exitForUpgrade(latestVersion, installed, opts = {}) {
|
|
761
|
+
const log2 = opts.log ?? ((msg) => console.error(msg));
|
|
762
|
+
log2(formatUpgradeInstructions(latestVersion));
|
|
763
|
+
log2(` Installed: ${installed}`);
|
|
764
|
+
log2(` Required: ${latestVersion} (or newer)`);
|
|
765
|
+
log2("");
|
|
766
|
+
const exit = opts.exitFn ?? ((code) => process.exit(code));
|
|
767
|
+
return exit(EXIT_TERMINAL);
|
|
768
|
+
}
|
|
675
769
|
async function enforceRunnerUpToDate(currentVersion, opts = {}) {
|
|
676
770
|
const log2 = opts.log ?? ((msg) => console.error(msg));
|
|
677
771
|
const result = await checkRunnerVersion(currentVersion, opts);
|
|
@@ -682,12 +776,7 @@ async function enforceRunnerUpToDate(currentVersion, opts = {}) {
|
|
|
682
776
|
return result;
|
|
683
777
|
}
|
|
684
778
|
if (result.outdated && result.latestVersion) {
|
|
685
|
-
|
|
686
|
-
log2(` Installed: ${result.currentVersion}`);
|
|
687
|
-
log2(` Required: ${result.latestVersion} (or newer)`);
|
|
688
|
-
log2("");
|
|
689
|
-
const exit = opts.exitFn ?? ((code) => process.exit(code));
|
|
690
|
-
exit(EXIT_TERMINAL);
|
|
779
|
+
exitForUpgrade(result.latestVersion, result.currentVersion, { exitFn: opts.exitFn, log: log2 });
|
|
691
780
|
}
|
|
692
781
|
return result;
|
|
693
782
|
}
|
|
@@ -695,6 +784,19 @@ function upgradeAction(activeRuns) {
|
|
|
695
784
|
return activeRuns > 0 ? "drain" : "exit";
|
|
696
785
|
}
|
|
697
786
|
|
|
787
|
+
// src/browser-pool-failure.ts
|
|
788
|
+
var MISSING_EXECUTABLE2 = /Executable doesn't exist at /i;
|
|
789
|
+
function browserPoolInitFailure(message, playwrightVersion) {
|
|
790
|
+
if (MISSING_EXECUTABLE2.test(message)) {
|
|
791
|
+
const pkg2 = playwrightVersion ? `playwright@${playwrightVersion}` : "playwright";
|
|
792
|
+
return {
|
|
793
|
+
exitCode: EXIT_TERMINAL,
|
|
794
|
+
advice: `Chromium is not installed for this runner. Install it, then run start again: npx ${pkg2} install chromium`
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
return { exitCode: 1, advice: "Discovery requires a browser pool. Fix the browser installation or set BROWSER_POOL_SIZE=0 to disable." };
|
|
798
|
+
}
|
|
799
|
+
|
|
698
800
|
// src/shutdown-signals.ts
|
|
699
801
|
var REPEAT_SIGNAL_WINDOW_MS = 1e3;
|
|
700
802
|
function installShutdownSignals(target, onShutdown, opts) {
|
|
@@ -6257,7 +6359,7 @@ async function harvestPlatformLocators(options) {
|
|
|
6257
6359
|
let reachedOrder = null;
|
|
6258
6360
|
let environmentalError;
|
|
6259
6361
|
log2(`Cross-platform locator discovery \u2014 resolving ${platform3} locators for ${harvestSteps.length} steps`);
|
|
6260
|
-
const
|
|
6362
|
+
const session2 = await createMobileDiscoverySession({
|
|
6261
6363
|
appId: options.target.appId,
|
|
6262
6364
|
platform: platform3,
|
|
6263
6365
|
platformVersion: options.target.platformVersion,
|
|
@@ -6266,13 +6368,13 @@ async function harvestPlatformLocators(options) {
|
|
|
6266
6368
|
launchMode: "LAUNCH_APP"
|
|
6267
6369
|
});
|
|
6268
6370
|
try {
|
|
6269
|
-
options.onSessionReady?.(
|
|
6270
|
-
await freshLaunchTargetApp(
|
|
6271
|
-
await dismissSystemUiAfterColdBoot(
|
|
6371
|
+
options.onSessionReady?.(session2.sessionId, session2.deviceId, platform3);
|
|
6372
|
+
await freshLaunchTargetApp(session2.sessionId, platform3, options.target.appId, log2);
|
|
6373
|
+
await dismissSystemUiAfterColdBoot(session2.sessionId, platform3, log2);
|
|
6272
6374
|
if (options.target.launchMode === "DEEP_LINK" && options.target.deeplink) {
|
|
6273
|
-
await openDeepLink(
|
|
6375
|
+
await openDeepLink(session2.sessionId, platform3, options.target.deeplink, options.target.appId);
|
|
6274
6376
|
}
|
|
6275
|
-
await restoreTargetAppIfExternal(
|
|
6377
|
+
await restoreTargetAppIfExternal(session2.sessionId, { appId: options.target.appId, platform: platform3 }, log2, "harvest start", false);
|
|
6276
6378
|
for (const step of harvestSteps) {
|
|
6277
6379
|
if (step.action === "launchApp") continue;
|
|
6278
6380
|
if (!stepAppliesToPlatform(step, platform3)) continue;
|
|
@@ -6282,7 +6384,7 @@ async function harvestPlatformLocators(options) {
|
|
|
6282
6384
|
if (!webViewCss && identityStrategies.length > 0 && step.target) {
|
|
6283
6385
|
try {
|
|
6284
6386
|
elementId = await findElement(
|
|
6285
|
-
|
|
6387
|
+
session2.sessionId,
|
|
6286
6388
|
step.target,
|
|
6287
6389
|
Math.max(DEFAULT_STEP_TIMEOUT_MS, WEBVIEW_HYDRATION_TIMEOUT_MS),
|
|
6288
6390
|
{
|
|
@@ -6296,7 +6398,7 @@ async function harvestPlatformLocators(options) {
|
|
|
6296
6398
|
}
|
|
6297
6399
|
}
|
|
6298
6400
|
if (elementId) {
|
|
6299
|
-
const bundle = await readNativeElementBundle(
|
|
6401
|
+
const bundle = await readNativeElementBundle(session2.sessionId, elementId, platform3);
|
|
6300
6402
|
if (bundle) {
|
|
6301
6403
|
bundles[step.order] = bundle;
|
|
6302
6404
|
log2(` \u2713 Step ${step.order} (${step.action}) resolved on ${platform3}`);
|
|
@@ -6308,20 +6410,20 @@ async function harvestPlatformLocators(options) {
|
|
|
6308
6410
|
divergentOrders.push(step.order);
|
|
6309
6411
|
log2(` \u26A0 Step ${step.order} (${step.action}) could not be resolved on ${platform3} \u2014 needs manual authoring`);
|
|
6310
6412
|
}
|
|
6311
|
-
const advanced = await driveHarvestStep(
|
|
6413
|
+
const advanced = await driveHarvestStep(session2.sessionId, step, elementId, platform3);
|
|
6312
6414
|
if (!advanced && isStateAdvancingAction(step.action)) {
|
|
6313
6415
|
log2(` \u2717 Could not advance step ${step.order} (${step.action}) on ${platform3}; stopping harvest early`);
|
|
6314
6416
|
break;
|
|
6315
6417
|
}
|
|
6316
6418
|
reachedOrder = step.order;
|
|
6317
|
-
await restoreTargetAppIfExternal(
|
|
6419
|
+
await restoreTargetAppIfExternal(session2.sessionId, { appId: options.target.appId, platform: platform3 }, log2, `harvest step ${step.order}`, false);
|
|
6318
6420
|
}
|
|
6319
6421
|
} catch (err) {
|
|
6320
6422
|
environmentalError = err instanceof Error ? err.message : String(err);
|
|
6321
6423
|
log2(` \u2717 Harvest aborted: ${environmentalError}`);
|
|
6322
6424
|
} finally {
|
|
6323
|
-
options.onSessionEnding?.(
|
|
6324
|
-
await
|
|
6425
|
+
options.onSessionEnding?.(session2.deviceId);
|
|
6426
|
+
await session2.stop().catch(() => {
|
|
6325
6427
|
});
|
|
6326
6428
|
}
|
|
6327
6429
|
return {
|
|
@@ -6329,8 +6431,8 @@ async function harvestPlatformLocators(options) {
|
|
|
6329
6431
|
bundles,
|
|
6330
6432
|
divergentOrders,
|
|
6331
6433
|
reachedOrder,
|
|
6332
|
-
deviceId:
|
|
6333
|
-
deviceName:
|
|
6434
|
+
deviceId: session2.deviceId,
|
|
6435
|
+
deviceName: session2.deviceName,
|
|
6334
6436
|
logs: logLines.join("\n"),
|
|
6335
6437
|
...environmentalError ? { environmentalError } : {}
|
|
6336
6438
|
};
|
|
@@ -8593,6 +8695,7 @@ function readActiveBundleId2(result) {
|
|
|
8593
8695
|
}
|
|
8594
8696
|
|
|
8595
8697
|
// src/services/doctor.ts
|
|
8698
|
+
var import_runner_core7 = __toESM(require_dist());
|
|
8596
8699
|
import { execSync } from "child_process";
|
|
8597
8700
|
import { existsSync as existsSync3 } from "fs";
|
|
8598
8701
|
import { platform as platform2 } from "os";
|
|
@@ -8623,7 +8726,10 @@ function computeDoctorReadiness(web, ios, android) {
|
|
|
8623
8726
|
const webReady = web.every((check2) => check2.ok);
|
|
8624
8727
|
const iosReady = ios.length > 0 && ios.every((check2) => check2.ok);
|
|
8625
8728
|
const androidReady = android.length > 0 && android.every((check2) => check2.ok);
|
|
8626
|
-
return { allPassed: webReady && (iosReady || androidReady), iosReady, androidReady };
|
|
8729
|
+
return { allPassed: webReady && (iosReady || androidReady), webReady, iosReady, androidReady };
|
|
8730
|
+
}
|
|
8731
|
+
function doctorExitCode(report, opts = {}) {
|
|
8732
|
+
return (opts.mobile ? report.allPassed : report.webReady) ? 0 : 1;
|
|
8627
8733
|
}
|
|
8628
8734
|
function run(cmd) {
|
|
8629
8735
|
try {
|
|
@@ -8640,17 +8746,33 @@ function check(name, fn) {
|
|
|
8640
8746
|
return { name, ok: false, detail: err instanceof Error ? err.message : "Unknown error" };
|
|
8641
8747
|
}
|
|
8642
8748
|
}
|
|
8643
|
-
function
|
|
8749
|
+
function bundledPlaywrightVersion() {
|
|
8750
|
+
try {
|
|
8751
|
+
const pkg2 = __require("playwright/package.json");
|
|
8752
|
+
return typeof pkg2.version === "string" && pkg2.version ? pkg2.version : null;
|
|
8753
|
+
} catch {
|
|
8754
|
+
return null;
|
|
8755
|
+
}
|
|
8756
|
+
}
|
|
8757
|
+
var DEFAULT_WEB_DEPS = {
|
|
8758
|
+
nodeVersion: process.version,
|
|
8759
|
+
playwrightVersion: bundledPlaywrightVersion,
|
|
8760
|
+
chromiumPath: import_runner_core7.getFullChromiumPath
|
|
8761
|
+
};
|
|
8762
|
+
function checkWeb(deps = DEFAULT_WEB_DEPS) {
|
|
8644
8763
|
const results = [];
|
|
8645
8764
|
results.push(check("Node.js", () => {
|
|
8646
|
-
const version =
|
|
8765
|
+
const version = deps.nodeVersion;
|
|
8647
8766
|
const major = parseInt(version.slice(1).split(".")[0], 10);
|
|
8648
8767
|
return { ok: major >= REQUIRED_NODE_MAJOR, detail: `${version}${major < REQUIRED_NODE_MAJOR ? ` (>= ${REQUIRED_NODE_MAJOR} required)` : ""}` };
|
|
8649
8768
|
}));
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8769
|
+
const playwrightVersion = deps.playwrightVersion();
|
|
8770
|
+
results.push(check("Playwright", () => playwrightVersion ? { ok: true, detail: `${playwrightVersion} (bundled with the runner)` } : { ok: false, detail: "The runner's Playwright could not be read. Reinstall the runner: npx @validate.qa/runner@latest" }));
|
|
8771
|
+
results.push(check("Chromium", () => {
|
|
8772
|
+
const path = deps.chromiumPath();
|
|
8773
|
+
if (path) return { ok: true, detail: path };
|
|
8774
|
+
const pkg2 = playwrightVersion ? `playwright@${playwrightVersion}` : "playwright";
|
|
8775
|
+
return { ok: false, detail: `Not installed for this runner. Run: npx ${pkg2} install chromium` };
|
|
8654
8776
|
}));
|
|
8655
8777
|
return results;
|
|
8656
8778
|
}
|
|
@@ -8796,10 +8918,10 @@ function runDoctor() {
|
|
|
8796
8918
|
const ios = checkIOS();
|
|
8797
8919
|
const android = checkAndroid();
|
|
8798
8920
|
const tunnel = checkTunnel();
|
|
8799
|
-
const { allPassed } = computeDoctorReadiness(web, ios, android);
|
|
8800
|
-
return { web, ios, android, tunnel, allPassed };
|
|
8921
|
+
const { allPassed, webReady } = computeDoctorReadiness(web, ios, android);
|
|
8922
|
+
return { web, ios, android, tunnel, allPassed, webReady };
|
|
8801
8923
|
}
|
|
8802
|
-
function printDoctorReport(report) {
|
|
8924
|
+
function printDoctorReport(report, opts = {}) {
|
|
8803
8925
|
console.log("");
|
|
8804
8926
|
console.log(`${C.bold}${C.cyan}\u{1F3E5} validate.qa Doctor${C.reset}`);
|
|
8805
8927
|
console.log(`${C.gray}${"\u2500".repeat(40)}${C.reset}`);
|
|
@@ -8818,12 +8940,15 @@ function printDoctorReport(report) {
|
|
|
8818
8940
|
console.log("");
|
|
8819
8941
|
if (report.allPassed) {
|
|
8820
8942
|
console.log(` ${C.green}\u2705 Web plus at least one mobile platform is ready${C.reset}`);
|
|
8943
|
+
} else if (report.webReady && !opts.mobile) {
|
|
8944
|
+
console.log(` ${C.green}\u2705 Web testing is ready${C.reset}`);
|
|
8945
|
+
console.log(` ${C.gray} No complete iOS or Android toolchain: needed only for mobile tests (check with: doctor --mobile)${C.reset}`);
|
|
8821
8946
|
} else {
|
|
8822
8947
|
const readiness = computeDoctorReadiness(report.web, report.ios, report.android);
|
|
8823
8948
|
const webFailures = report.web.filter((check2) => !check2.ok).length;
|
|
8824
8949
|
let reason;
|
|
8825
8950
|
if (webFailures > 0) {
|
|
8826
|
-
reason = `${webFailures} web prerequisite${webFailures === 1 ? "" : "s"}
|
|
8951
|
+
reason = `${webFailures} web prerequisite${webFailures === 1 ? " needs" : "s need"} attention`;
|
|
8827
8952
|
} else if (!readiness.iosReady && !readiness.androidReady) {
|
|
8828
8953
|
reason = "No complete iOS or Android toolchain is ready";
|
|
8829
8954
|
} else {
|
|
@@ -9167,6 +9292,13 @@ function attachMobileDiscoveryNetworkContext(calls, result, appId) {
|
|
|
9167
9292
|
}));
|
|
9168
9293
|
}
|
|
9169
9294
|
var runnerPackage = require_package();
|
|
9295
|
+
function bundledPlaywrightVersion2() {
|
|
9296
|
+
try {
|
|
9297
|
+
return __require("playwright-core/package.json").version;
|
|
9298
|
+
} catch {
|
|
9299
|
+
return void 0;
|
|
9300
|
+
}
|
|
9301
|
+
}
|
|
9170
9302
|
var MIRROR_RELAY_FRESH_MS = 3e3;
|
|
9171
9303
|
var liveFrameWatchedByProject = /* @__PURE__ */ new Map();
|
|
9172
9304
|
var liveFrameIdleProbeAt = /* @__PURE__ */ new Map();
|
|
@@ -9911,7 +10043,7 @@ function pruneCollectedPageForPost(page) {
|
|
|
9911
10043
|
return out;
|
|
9912
10044
|
}
|
|
9913
10045
|
function discoveryRenderTypeFields(input) {
|
|
9914
|
-
if (input.err instanceof
|
|
10046
|
+
if (input.err instanceof import_runner_core8.UnsupportedRendererError) {
|
|
9915
10047
|
return { detectedAppRenderType: input.err.renderType, appRenderDetectionSignals: input.err.signals };
|
|
9916
10048
|
}
|
|
9917
10049
|
return input.appRenderType ? { detectedAppRenderType: input.appRenderType } : {};
|
|
@@ -10092,8 +10224,8 @@ function toLiveBrowserMode(mode) {
|
|
|
10092
10224
|
async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = () => {
|
|
10093
10225
|
}, onMobileHealPending = () => {
|
|
10094
10226
|
}, assignedDeviceId, mobileLeaseError, signal) {
|
|
10095
|
-
if (signal && (0,
|
|
10096
|
-
return (0,
|
|
10227
|
+
if (signal && (0, import_runner_core10.currentRunAbortSignal)() !== signal) {
|
|
10228
|
+
return (0, import_runner_core10.runWithAbortScope)({ signal, runId: run2.runId }, () => executeRun(run2, opts, headers, requestLiveBrowserHeartbeat, onMobileHealPending, assignedDeviceId, mobileLeaseError, signal));
|
|
10097
10229
|
}
|
|
10098
10230
|
const runMode = run2.mode ?? "run";
|
|
10099
10231
|
const RUNNER_MODE = opts.runnerMode ?? "playwright-native";
|
|
@@ -10154,7 +10286,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10154
10286
|
...nativeOptions,
|
|
10155
10287
|
liveBrowserHandle,
|
|
10156
10288
|
onLiveBrowserSnapshot: requestLiveBrowserHeartbeat,
|
|
10157
|
-
signal: (0,
|
|
10289
|
+
signal: (0, import_runner_core10.linkRunAbort)(nativeOptions.signal) ?? signal
|
|
10158
10290
|
});
|
|
10159
10291
|
const callbackContext = {
|
|
10160
10292
|
runId: run2.runId,
|
|
@@ -10167,7 +10299,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10167
10299
|
};
|
|
10168
10300
|
const startTime = Date.now();
|
|
10169
10301
|
try {
|
|
10170
|
-
(0,
|
|
10302
|
+
(0, import_runner_core10.throwIfRunAborted)();
|
|
10171
10303
|
if (mobileLeaseError) {
|
|
10172
10304
|
await postRunResult({
|
|
10173
10305
|
status: "ERROR",
|
|
@@ -10203,7 +10335,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10203
10335
|
const requireCreds = authPlan.needsLoginCreds && !legacyHeaderBypass && !isHallucinationShape && !selfProvisioningRegisterTest;
|
|
10204
10336
|
if (requireCreds && !run2.testVariables?.["TEST_USER_EMAIL"]?.trim()) {
|
|
10205
10337
|
preflightError = "Pre-flight failed: Missing TEST_USER_EMAIL. Tests that log in (tagged @auth, @needs-auth, @auth-cookie-state, @auth-login-cookie, or @auth-login-bearer) or read TEST_USER_* in their code require login credentials.";
|
|
10206
|
-
} else if (requireCreds && !run2.testVariables?.["TEST_USER_PASSWORD"]?.trim()) {
|
|
10338
|
+
} else if (requireCreds && !(codeProvided && (0, import_inbox_stamp.codeSignsInWithoutPassword)(code)) && !run2.testVariables?.["TEST_USER_PASSWORD"]?.trim()) {
|
|
10207
10339
|
preflightError = "Pre-flight failed: Missing TEST_USER_PASSWORD. Tests that log in (tagged @auth, @needs-auth, @auth-cookie-state, @auth-login-cookie, or @auth-login-bearer) or read TEST_USER_* in their code require login credentials.";
|
|
10208
10340
|
}
|
|
10209
10341
|
}
|
|
@@ -10225,7 +10357,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10225
10357
|
} else if (preflight.note) {
|
|
10226
10358
|
log.info(preflight.note);
|
|
10227
10359
|
}
|
|
10228
|
-
tlsCertNote = (0, import_tls_cert_probe.targetCertificateNote)(run2.baseUrl);
|
|
10360
|
+
tlsCertNote = (0, import_tls_cert_probe.targetCertificateNote)(run2.baseUrl, { specCode: run2.playwrightCode });
|
|
10229
10361
|
void tlsCertNote.then((note) => {
|
|
10230
10362
|
if (note) log.warn(note);
|
|
10231
10363
|
});
|
|
@@ -10287,7 +10419,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10287
10419
|
releaseAuthLock2
|
|
10288
10420
|
);
|
|
10289
10421
|
const duration2 = Date.now() - startTime;
|
|
10290
|
-
(0,
|
|
10422
|
+
(0, import_runner_core10.throwIfRunAborted)();
|
|
10291
10423
|
await postRunResult({
|
|
10292
10424
|
status: result2.passed ? "PASSED" : "FAILED",
|
|
10293
10425
|
apiCalls: result2.apiCalls,
|
|
@@ -10332,10 +10464,10 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10332
10464
|
completionTokens: info.completionTokens,
|
|
10333
10465
|
totalTokens: info.totalTokens,
|
|
10334
10466
|
requestBody: { iteration: info.iteration, toolCalls: info.toolCalls, session: info.requestMessages?.[0] ?? null },
|
|
10335
|
-
metadata: { phase: "heal", groupId: info.groupId, iteration: info.iteration, healMode: "batch", ...(0,
|
|
10467
|
+
metadata: { phase: "heal", groupId: info.groupId, iteration: info.iteration, healMode: "batch", ...(0, import_runner_core10.cliAuditMetadata)(info) }
|
|
10336
10468
|
});
|
|
10337
10469
|
};
|
|
10338
|
-
const batchResult = await (0,
|
|
10470
|
+
const batchResult = await (0, import_runner_core10.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
|
|
10339
10471
|
onAICall: batchHealAudit,
|
|
10340
10472
|
testVariables: run2.testVariables,
|
|
10341
10473
|
runNativeTest: runNativeWithLiveBrowser,
|
|
@@ -10344,7 +10476,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10344
10476
|
saveRefreshedLogin: grokLogin?.saveRefreshed,
|
|
10345
10477
|
engineSpec: run2.healerModel,
|
|
10346
10478
|
// Returns HEAL_TIMEOUT: for the tests not reached instead of dying at the runner's batch-heal deadline.
|
|
10347
|
-
deadlineAt: (0,
|
|
10479
|
+
deadlineAt: (0, import_runner_core8.batchHealDeadlineAt)(startTime),
|
|
10348
10480
|
onLog: healLogStream.onLog,
|
|
10349
10481
|
...batchHealPartialHooks(opts.serverUrl, headers, run2.runId, run2.projectId || void 0, void 0, run2.claimEpoch)
|
|
10350
10482
|
});
|
|
@@ -10354,7 +10486,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10354
10486
|
} catch (drainErr) {
|
|
10355
10487
|
console.warn(`[batch-heal] audit flush failed: ${drainErr instanceof Error ? drainErr.message : String(drainErr)}`);
|
|
10356
10488
|
}
|
|
10357
|
-
(0,
|
|
10489
|
+
(0, import_runner_core10.throwIfRunAborted)();
|
|
10358
10490
|
try {
|
|
10359
10491
|
await postBatchHealResult(opts.serverUrl, headers, run2.runId, {
|
|
10360
10492
|
perTest: batchResult.perTest,
|
|
@@ -10362,7 +10494,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10362
10494
|
logs: batchResult.logs,
|
|
10363
10495
|
durationMs: batchResult.totalDurationMs
|
|
10364
10496
|
}, RESULT_POST_BACKOFF_BASE_MS, run2.claimEpoch);
|
|
10365
|
-
log.ok((0,
|
|
10497
|
+
log.ok((0, import_runner_core10.batchHealCompleteLine)(batchResult.perTest, tests.length, batchResult.totalDurationMs));
|
|
10366
10498
|
} catch (err) {
|
|
10367
10499
|
log.fail(`Failed to post batch heal result: ${err instanceof Error ? err.message : String(err)}`);
|
|
10368
10500
|
}
|
|
@@ -10395,7 +10527,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10395
10527
|
}, run2.runId) : void 0;
|
|
10396
10528
|
const apiHealStorageStatePath = apiHealAuthPaths ? (0, import_local_auth_state.getValidLocalAuthState)(apiHealAuthPaths.cachePath) : void 0;
|
|
10397
10529
|
if (apiHealStorageStatePath) log.info("API heal verification is using saved auth state");
|
|
10398
|
-
const apiHealAuthSession = (0,
|
|
10530
|
+
const apiHealAuthSession = (0, import_runner_core9.sharedAuthSessionStamp)(apiHealStorageStatePath, false, sharedAuthInjectEnabled());
|
|
10399
10531
|
const apiHealAuthPlan = authPlanFor(run2.tags, run2.playwrightCode);
|
|
10400
10532
|
const stickyAppBugs = /* @__PURE__ */ new Map();
|
|
10401
10533
|
const lastChangeByTitle = /* @__PURE__ */ new Map();
|
|
@@ -10512,10 +10644,10 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10512
10644
|
}
|
|
10513
10645
|
if (result3.verdict === "app_bug") {
|
|
10514
10646
|
const duration4 = Date.now() - startTime;
|
|
10515
|
-
const legacyCold = (0,
|
|
10647
|
+
const legacyCold = (0, import_runner_core9.apiHealColdSessionRelabel)("legacy", {
|
|
10516
10648
|
tags: run2.tags ?? [],
|
|
10517
10649
|
authStateExpected: apiHealAuthPlan.needsCookieState,
|
|
10518
|
-
authStateInjected: (0,
|
|
10650
|
+
authStateInjected: (0, import_runner_core9.storageStateDelivered)(apiHealStorageStatePath, finalVerifyResult),
|
|
10519
10651
|
needsStandingHeaders: apiHealAuthPlan.needsStandingHeaders,
|
|
10520
10652
|
standingHeadersInjected: !!finalVerifyResult?.authDelivery?.standingHeadersInjected,
|
|
10521
10653
|
error: result3.bugDescription ?? null,
|
|
@@ -10588,10 +10720,14 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10588
10720
|
const allAppBug = stepCount > 0 && enrichedSteps.every((s) => s.verdict === "app_bug");
|
|
10589
10721
|
const hasFixFailed = enrichedSteps.some((s) => s.verdict === "fix_failed");
|
|
10590
10722
|
const hasPass = enrichedSteps.some((s) => s.verdict === "pass");
|
|
10591
|
-
const
|
|
10723
|
+
const verifyPassed = stepCount > 0 ? finalVerifyResult?.passed === true && hasPass && !hasFixFailed && !allAppBug : finalVerifyResult?.passed === true;
|
|
10724
|
+
const partialSkipError = verifyPassed ? (0, import_runner_core8.apiHealPartialSkipError)(enrichedSteps) : void 0;
|
|
10725
|
+
const passed = verifyPassed && partialSkipError === void 0;
|
|
10592
10726
|
let composedError;
|
|
10593
10727
|
if (!passed) {
|
|
10594
|
-
if (
|
|
10728
|
+
if (partialSkipError !== void 0) {
|
|
10729
|
+
composedError = partialSkipError;
|
|
10730
|
+
} else if (finalUnfixableReason) {
|
|
10595
10731
|
composedError = `Unfixable after ${attemptsUsed} attempt(s): ${finalUnfixableReason}`;
|
|
10596
10732
|
} else if (allAppBug || enrichedSteps.some((s) => s.verdict === "app_bug") && !hasFixFailed) {
|
|
10597
10733
|
const bugs = enrichedSteps.filter((s) => s.verdict === "app_bug");
|
|
@@ -10603,11 +10739,21 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10603
10739
|
let apiHealStatus = passed ? "PASSED" : "FAILED";
|
|
10604
10740
|
let apiHealError = composedError;
|
|
10605
10741
|
if (!passed) {
|
|
10606
|
-
if (finalUnfixableReason?.startsWith(
|
|
10742
|
+
if (finalUnfixableReason?.startsWith(import_runner_core8.HEAL_AI_NO_ANSWER_ERROR_PREFIX) || (0, import_runner_core8.isHealAiQuotaError)(finalUnfixableReason)) {
|
|
10607
10743
|
apiHealStatus = "ERROR";
|
|
10608
10744
|
apiHealError = finalUnfixableReason;
|
|
10609
10745
|
} else if (isConfigLikeReason(classifyErrorMessage(composedError ?? null))) {
|
|
10610
|
-
|
|
10746
|
+
const configIssueRefusal2 = (0, import_runner_core8.refuseConfigIssueOnAppServerError)({
|
|
10747
|
+
error: composedError,
|
|
10748
|
+
baseUrl: run2.apiBaseUrl ?? run2.baseUrl,
|
|
10749
|
+
calls: (0, import_runner_core8.healResultCalls)({ stepResults: rawSteps })
|
|
10750
|
+
});
|
|
10751
|
+
if (configIssueRefusal2) {
|
|
10752
|
+
apiHealError = configIssueRefusal2.error;
|
|
10753
|
+
log.warn(`\u26A0 API-heal config-issue verdict refused: ${configIssueRefusal2.evidence}`);
|
|
10754
|
+
} else {
|
|
10755
|
+
apiHealStatus = "ERROR";
|
|
10756
|
+
}
|
|
10611
10757
|
} else {
|
|
10612
10758
|
const rlDetection = (0, import_rate_limit_detector.detectRateLimitBlock)({
|
|
10613
10759
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
@@ -10641,10 +10787,10 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10641
10787
|
}
|
|
10642
10788
|
}
|
|
10643
10789
|
if (apiHealStatus === "FAILED") {
|
|
10644
|
-
const cold = (0,
|
|
10790
|
+
const cold = (0, import_runner_core9.apiHealColdSessionRelabel)("verified", {
|
|
10645
10791
|
tags: run2.tags ?? [],
|
|
10646
10792
|
authStateExpected: apiHealAuthPlan.needsCookieState,
|
|
10647
|
-
authStateInjected: (0,
|
|
10793
|
+
authStateInjected: (0, import_runner_core9.storageStateDelivered)(apiHealStorageStatePath, finalVerifyResult),
|
|
10648
10794
|
needsStandingHeaders: apiHealAuthPlan.needsStandingHeaders,
|
|
10649
10795
|
standingHeadersInjected: !!finalVerifyResult?.authDelivery?.standingHeadersInjected,
|
|
10650
10796
|
error: apiHealError ?? finalVerifyResult?.error,
|
|
@@ -10707,11 +10853,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10707
10853
|
completionTokens: info.completionTokens,
|
|
10708
10854
|
totalTokens: info.totalTokens,
|
|
10709
10855
|
...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
|
|
10710
|
-
metadata: { groupId: info.groupId, iteration: info.iteration, testName: run2.testCaseId, ...(0,
|
|
10856
|
+
metadata: { groupId: info.groupId, iteration: info.iteration, testName: run2.testCaseId, ...(0, import_runner_core10.cliAuditMetadata)(info) }
|
|
10711
10857
|
});
|
|
10712
10858
|
};
|
|
10713
10859
|
const envOverride = (process.env.USE_GROK_PER_TEST_HEAL ?? "").trim().toLowerCase();
|
|
10714
|
-
const useGrokHeal = envOverride === "true" ? true : envOverride === "false" ? false : (0,
|
|
10860
|
+
const useGrokHeal = envOverride === "true" ? true : envOverride === "false" ? false : (0, import_runner_core10.getAgenticEngine)(run2.healerModel) === "grok-build";
|
|
10715
10861
|
const healOpts = {
|
|
10716
10862
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
10717
10863
|
tags: run2.tags ?? [],
|
|
@@ -10734,19 +10880,19 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10734
10880
|
// Whole-heal deadline: the runner's heal deadline minus a margin for the result
|
|
10735
10881
|
// post (heal-deadline.ts). The healer stops both phases inside it and returns a
|
|
10736
10882
|
// HEAL_TIMEOUT: result instead of dying as a reclaimed task.
|
|
10737
|
-
deadlineAt: (0,
|
|
10883
|
+
deadlineAt: (0, import_runner_core8.healDeadlineAt)(startTime)
|
|
10738
10884
|
};
|
|
10739
10885
|
if (useGrokHeal) {
|
|
10740
10886
|
const source = envOverride === "true" ? "env USE_GROK_PER_TEST_HEAL=true" : `healerModel=${run2.healerModel ?? "null"}`;
|
|
10741
10887
|
log.info(`Heal engine: GROK (${source})`);
|
|
10742
10888
|
}
|
|
10743
10889
|
const grokHealLogStream = useGrokHeal ? createHealLogStreamer(opts.serverUrl, headers, run2.runId) : null;
|
|
10744
|
-
const httpFallback = run2.healerFallbackModel && (0,
|
|
10890
|
+
const httpFallback = run2.healerFallbackModel && (0, import_runner_core10.getAgenticEngine)(run2.healerFallbackModel) === "native" ? run2.healerFallbackModel : null;
|
|
10745
10891
|
let result2;
|
|
10746
10892
|
if (useGrokHeal) {
|
|
10747
10893
|
try {
|
|
10748
10894
|
const grokLogin = await (0, import_fetch_cli_subscription.fetchCliSubscription)({ serverUrl: opts.serverUrl, runId: run2.runId, headers, cli: "grok-build", onLog: grokHealLogStream?.onLog });
|
|
10749
|
-
result2 = await (0,
|
|
10895
|
+
result2 = await (0, import_runner_core10.executeGrokPerTestHeal)(run2.playwrightCode, run2.steps, run2.baseUrl, {
|
|
10750
10896
|
...healOpts,
|
|
10751
10897
|
subscriptionSecret: grokLogin?.value,
|
|
10752
10898
|
saveRefreshedLogin: grokLogin?.saveRefreshed,
|
|
@@ -10790,17 +10936,20 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10790
10936
|
[audit] ${drainErr instanceof Error ? drainErr.message : String(drainErr)}`;
|
|
10791
10937
|
console.warn(`[runner]${auditDrainError}`);
|
|
10792
10938
|
}
|
|
10793
|
-
const
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
const
|
|
10939
|
+
const relabelSteps = result2.relabelEvidenceSteps ?? result2.stepResults;
|
|
10940
|
+
const configIssueRefusal = result2.passed ? null : (0, import_runner_core8.refuseConfigIssueOnAppServerError)({ error: result2.error, baseUrl: run2.baseUrl, calls: (0, import_runner_core8.healResultCalls)(result2) });
|
|
10941
|
+
if (configIssueRefusal) log.warn(`\u26A0 Heal config-issue verdict refused: ${configIssueRefusal.evidence}`);
|
|
10942
|
+
const isConfigIssue = !result2.passed && !configIssueRefusal && isConfigLikeReason(classifyErrorMessage(result2.error ?? null));
|
|
10943
|
+
let healStatus = (0, import_runner_core8.healRunStatus)({ passed: result2.passed, error: configIssueRefusal?.error ?? result2.error, isConfigIssue });
|
|
10944
|
+
let healError = configIssueRefusal?.error ?? result2.error;
|
|
10945
|
+
const rateLimitRelabel = (0, import_runner_core9.uiHealRateLimitRelabel)({
|
|
10797
10946
|
passed: result2.passed,
|
|
10798
10947
|
isConfigIssue,
|
|
10799
10948
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
10800
10949
|
tags: run2.tags ?? [],
|
|
10801
10950
|
error: result2.error,
|
|
10802
10951
|
logs: result2.logs,
|
|
10803
|
-
stepResults:
|
|
10952
|
+
stepResults: relabelSteps,
|
|
10804
10953
|
apiCalls: result2.apiCalls
|
|
10805
10954
|
});
|
|
10806
10955
|
if (rateLimitRelabel) {
|
|
@@ -10808,13 +10957,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10808
10957
|
healError = rateLimitRelabel.error;
|
|
10809
10958
|
log.warn(rateLimitRelabel.logLine);
|
|
10810
10959
|
}
|
|
10811
|
-
const loginRelabel = (0,
|
|
10960
|
+
const loginRelabel = (0, import_runner_core9.uiHealLoginRelabel)({
|
|
10812
10961
|
status: healStatus,
|
|
10813
10962
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
10814
10963
|
tags: run2.tags ?? [],
|
|
10815
10964
|
error: healError,
|
|
10816
10965
|
logs: result2.logs,
|
|
10817
|
-
stepResults:
|
|
10966
|
+
stepResults: relabelSteps
|
|
10818
10967
|
});
|
|
10819
10968
|
if (loginRelabel) {
|
|
10820
10969
|
healStatus = loginRelabel.status;
|
|
@@ -10883,7 +11032,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10883
11032
|
signal
|
|
10884
11033
|
});
|
|
10885
11034
|
const overall = (0, import_regression_runner.regressionSuiteOverall)(result2);
|
|
10886
|
-
(0,
|
|
11035
|
+
(0, import_runner_core10.throwIfRunAborted)();
|
|
10887
11036
|
await postRegressionResult(opts.serverUrl, headers, run2.runId, {
|
|
10888
11037
|
overall,
|
|
10889
11038
|
logs: result2.logs,
|
|
@@ -10913,9 +11062,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10913
11062
|
`${opts.serverUrl}/api/runner/runs/${run2.runId}/ai-audit-batch`,
|
|
10914
11063
|
headers
|
|
10915
11064
|
);
|
|
10916
|
-
const suiteQuotaFallback = (0,
|
|
11065
|
+
const suiteQuotaFallback = (0, import_runner_core8.createQuotaFallbackState)(run2.quotaFallbackModel);
|
|
10917
11066
|
for (const failedTest of failedTests) {
|
|
10918
|
-
(0,
|
|
11067
|
+
(0, import_runner_core10.throwIfRunAborted)();
|
|
10919
11068
|
const testStart = Date.now();
|
|
10920
11069
|
log.info(` Healing: ${failedTest.name ?? failedTest.testCaseId}`);
|
|
10921
11070
|
const isApiTest = Array.isArray(failedTest.tags) && failedTest.tags.includes("@api");
|
|
@@ -10928,7 +11077,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10928
11077
|
signal: AbortSignal.timeout(6e4)
|
|
10929
11078
|
});
|
|
10930
11079
|
const healBody = healRes.ok ? await healRes.json() : null;
|
|
10931
|
-
if (healBody?.verdict === "unfixable" && (0,
|
|
11080
|
+
if (healBody?.verdict === "unfixable" && (0, import_runner_core8.isHealAiQuotaError)(healBody.reason)) {
|
|
10932
11081
|
results.push({
|
|
10933
11082
|
testCaseId: failedTest.testCaseId,
|
|
10934
11083
|
status: "unfixable",
|
|
@@ -10948,15 +11097,16 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10948
11097
|
authStandingMode: failedTestPlan.standingHeadersMode,
|
|
10949
11098
|
validateContext: callbackContext
|
|
10950
11099
|
});
|
|
11100
|
+
const verdict = (0, import_runner_core8.apiHealVerifyVerdict)(verifyResult);
|
|
10951
11101
|
results.push({
|
|
10952
11102
|
testCaseId: failedTest.testCaseId,
|
|
10953
|
-
status:
|
|
10954
|
-
error:
|
|
11103
|
+
status: verdict.fixed ? "fixed" : "unfixable",
|
|
11104
|
+
error: verdict.error,
|
|
10955
11105
|
diagnosis: "API test healed via server-side AI",
|
|
10956
|
-
healedCode:
|
|
11106
|
+
healedCode: verdict.fixed ? healedCode : void 0,
|
|
10957
11107
|
duration: Date.now() - testStart
|
|
10958
11108
|
});
|
|
10959
|
-
if (
|
|
11109
|
+
if (verdict.fixed) {
|
|
10960
11110
|
log.ok(` Fixed: ${failedTest.name}`);
|
|
10961
11111
|
} else {
|
|
10962
11112
|
log.fail(` Could not fix: ${failedTest.name}`);
|
|
@@ -10993,12 +11143,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10993
11143
|
completionTokens: info.completionTokens,
|
|
10994
11144
|
totalTokens: info.totalTokens,
|
|
10995
11145
|
...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
|
|
10996
|
-
metadata: { groupId: info.groupId, testCaseId: failedTest.testCaseId, ...(0,
|
|
11146
|
+
metadata: { groupId: info.groupId, testCaseId: failedTest.testCaseId, ...(0, import_runner_core10.cliAuditMetadata)(info) }
|
|
10997
11147
|
});
|
|
10998
11148
|
};
|
|
10999
11149
|
try {
|
|
11000
11150
|
const envOverrideSuite = (process.env.USE_GROK_PER_TEST_HEAL ?? "").trim().toLowerCase();
|
|
11001
|
-
const useGrokHealSuite = envOverrideSuite === "true" ? true : envOverrideSuite === "false" ? false : (0,
|
|
11151
|
+
const useGrokHealSuite = envOverrideSuite === "true" ? true : envOverrideSuite === "false" ? false : (0, import_runner_core10.getAgenticEngine)(run2.healerModel) === "grok-build";
|
|
11002
11152
|
const suiteHealOpts = {
|
|
11003
11153
|
testName: failedTest.name ?? failedTest.testCaseId,
|
|
11004
11154
|
tags: failedTest.tags ?? [],
|
|
@@ -11020,12 +11170,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11020
11170
|
liveBrowserHandle,
|
|
11021
11171
|
runNativeTest: runNativeWithLiveBrowser
|
|
11022
11172
|
};
|
|
11023
|
-
const suiteHttpFallback = run2.healerFallbackModel && (0,
|
|
11173
|
+
const suiteHttpFallback = run2.healerFallbackModel && (0, import_runner_core10.getAgenticEngine)(run2.healerFallbackModel) === "native" ? run2.healerFallbackModel : null;
|
|
11024
11174
|
let healResult;
|
|
11025
11175
|
if (useGrokHealSuite) {
|
|
11026
11176
|
try {
|
|
11027
11177
|
const grokLogin = await (0, import_fetch_cli_subscription.fetchCliSubscription)({ serverUrl: opts.serverUrl, runId: run2.runId, headers, cli: "grok-build", onLog: log.warn });
|
|
11028
|
-
healResult = await (0,
|
|
11178
|
+
healResult = await (0, import_runner_core10.executeGrokPerTestHeal)(
|
|
11029
11179
|
failedTest.playwrightCode,
|
|
11030
11180
|
failedTest.steps ?? [],
|
|
11031
11181
|
run2.baseUrl,
|
|
@@ -11065,20 +11215,21 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11065
11215
|
});
|
|
11066
11216
|
log.ok(` Fixed: ${failedTest.name}`);
|
|
11067
11217
|
} else {
|
|
11068
|
-
const quotaStopped = (0,
|
|
11069
|
-
const
|
|
11070
|
-
const isAppBug = !
|
|
11071
|
-
const
|
|
11218
|
+
const quotaStopped = (0, import_runner_core8.isHealAiQuotaError)(healResult.error);
|
|
11219
|
+
const infraStopped = (0, import_runner_core8.isHealInfraStopError)(healResult.error);
|
|
11220
|
+
const isAppBug = !infraStopped && (0, import_runner_core8.isAppBugHealError)(healResult.error);
|
|
11221
|
+
const configIssueRefusal = infraStopped ? null : (0, import_runner_core8.refuseConfigIssueOnAppServerError)({ error: healResult.error, baseUrl: run2.baseUrl, calls: (0, import_runner_core8.healResultCalls)(healResult) });
|
|
11222
|
+
const isConfigIssue = !configIssueRefusal && isConfigLikeReason(classifyErrorMessage(healResult.error ?? null));
|
|
11072
11223
|
const status = isAppBug ? "app-bug" : isConfigIssue ? "config-issue" : "unfixable";
|
|
11073
|
-
const diagnosisMsg = isAppBug ? "Application error detected" : isConfigIssue ? "Configuration issue detected" :
|
|
11224
|
+
const diagnosisMsg = isAppBug ? "Application error detected" : isConfigIssue ? "Configuration issue detected" : configIssueRefusal ? configIssueRefusal.error : infraStopped && healResult.error ? healResult.error : "Could not heal after AI attempts";
|
|
11074
11225
|
results.push({
|
|
11075
11226
|
testCaseId: failedTest.testCaseId,
|
|
11076
11227
|
status,
|
|
11077
|
-
error: healResult.error,
|
|
11228
|
+
error: configIssueRefusal?.error ?? healResult.error,
|
|
11078
11229
|
diagnosis: diagnosisMsg,
|
|
11079
11230
|
duration: Date.now() - testStart
|
|
11080
11231
|
});
|
|
11081
|
-
log[isAppBug || isConfigIssue ||
|
|
11232
|
+
log[isAppBug || isConfigIssue || infraStopped ? "warn" : "fail"](` ${isAppBug ? "App bug" : isConfigIssue ? "Config issue" : quotaStopped ? "Not healed (AI provider quota)" : infraStopped ? `Not healed (${healResult.error.slice(0, healResult.error.indexOf(":"))})` : configIssueRefusal ? `Could not fix (config-issue verdict refused: ${configIssueRefusal.evidence})` : "Could not fix"}: ${failedTest.name}`);
|
|
11082
11233
|
}
|
|
11083
11234
|
} catch (err) {
|
|
11084
11235
|
results.push({
|
|
@@ -11091,7 +11242,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11091
11242
|
log.fail(` Heal crashed: ${failedTest.name}`);
|
|
11092
11243
|
}
|
|
11093
11244
|
}
|
|
11094
|
-
(0,
|
|
11245
|
+
(0, import_runner_core10.throwIfRunAborted)();
|
|
11095
11246
|
const lastResult = results[results.length - 1];
|
|
11096
11247
|
if (lastResult) {
|
|
11097
11248
|
fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/live-suite-progress`, {
|
|
@@ -11120,7 +11271,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11120
11271
|
auditDrainWarning = drainErr instanceof Error ? drainErr.message : String(drainErr);
|
|
11121
11272
|
console.warn(`[runner] ${auditDrainWarning}`);
|
|
11122
11273
|
}
|
|
11123
|
-
(0,
|
|
11274
|
+
(0, import_runner_core10.throwIfRunAborted)();
|
|
11124
11275
|
const liveRes = await postLiveSuiteResult(opts.serverUrl, headers, run2.runId, {
|
|
11125
11276
|
overall,
|
|
11126
11277
|
duration: totalDuration,
|
|
@@ -11295,7 +11446,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11295
11446
|
}
|
|
11296
11447
|
return await res.json();
|
|
11297
11448
|
};
|
|
11298
|
-
ctx.smsReserve = (0,
|
|
11449
|
+
ctx.smsReserve = (0, import_runner_core10.createHttpSmsReserveTransport)({ serverUrl: opts.serverUrl, headers, runId: run2.runId });
|
|
11299
11450
|
}
|
|
11300
11451
|
const canonicalBase = await (0, import_base_url_check.canonicalDiscoveryBaseUrl)(
|
|
11301
11452
|
{ runMode, baseUrl: ctx.baseUrl, native: ctx.medium === "IOS_NATIVE" || ctx.medium === "ANDROID_NATIVE" || run2.framework === "APPIUM" }
|
|
@@ -11357,7 +11508,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11357
11508
|
...info.exitReason ? { exitReason: info.exitReason } : {},
|
|
11358
11509
|
// Engine cost, engine id, step, cache + reasoning tokens: same helper as the server's
|
|
11359
11510
|
// Fargate path (execute-run.ts), so the two rows agree.
|
|
11360
|
-
...(0,
|
|
11511
|
+
...(0, import_runner_core10.cliAuditMetadata)(info),
|
|
11361
11512
|
...ledgerAudit ? {
|
|
11362
11513
|
ledger: ledgerAudit.entries,
|
|
11363
11514
|
ledgerEntryCount: ledgerAudit.originalLength,
|
|
@@ -11392,12 +11543,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11392
11543
|
}).catch(() => {
|
|
11393
11544
|
});
|
|
11394
11545
|
};
|
|
11395
|
-
const onScreenshot = async (route2, base64, visual) => {
|
|
11546
|
+
const onScreenshot = async (route2, base64, visual, capture) => {
|
|
11396
11547
|
try {
|
|
11397
11548
|
const resp = await fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/page-screenshot`, {
|
|
11398
11549
|
method: "POST",
|
|
11399
11550
|
headers: { ...headers, "Content-Type": "application/json" },
|
|
11400
|
-
body: JSON.stringify({ route: route2, base64, ...visual ? { visualName: visual.name } : {} }),
|
|
11551
|
+
body: JSON.stringify({ route: route2, base64, ...visual ? { visualName: visual.name } : {}, ...capture?.stillLoading ? { stillLoading: true } : {} }),
|
|
11401
11552
|
signal: AbortSignal.timeout(15e3)
|
|
11402
11553
|
});
|
|
11403
11554
|
if (!resp.ok) return null;
|
|
@@ -11666,7 +11817,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11666
11817
|
requestLiveBrowserHeartbeat(true);
|
|
11667
11818
|
mobileOnLog(`Mobile discovery (${ctx.mode}) \u2014 ${platform3} / ${mobileTarget.appId}`);
|
|
11668
11819
|
setExecutorBusy(true);
|
|
11669
|
-
let
|
|
11820
|
+
let session2 = null;
|
|
11670
11821
|
let capture = null;
|
|
11671
11822
|
let discoveryTeardownStarted = false;
|
|
11672
11823
|
try {
|
|
@@ -11687,7 +11838,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11687
11838
|
discoverySessionTarget,
|
|
11688
11839
|
onDeviceSelected ? { onDeviceSelected } : void 0
|
|
11689
11840
|
);
|
|
11690
|
-
|
|
11841
|
+
session2 = await openDiscoverySession(
|
|
11691
11842
|
async (device) => {
|
|
11692
11843
|
discoverySessionTarget.assignedDeviceId = device.deviceId;
|
|
11693
11844
|
liveBrowserHandle.patch({ deviceId: device.deviceId, deviceName: device.name });
|
|
@@ -11708,18 +11859,18 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11708
11859
|
}
|
|
11709
11860
|
}
|
|
11710
11861
|
);
|
|
11711
|
-
mobileOnLog(`Appium session created: ${
|
|
11712
|
-
attachMirrorWithRelay(
|
|
11862
|
+
mobileOnLog(`Appium session created: ${session2.sessionId} on device ${session2.deviceId}`);
|
|
11863
|
+
attachMirrorWithRelay(session2.sessionId, platform3, session2.deviceId);
|
|
11713
11864
|
const driver = new MobileBridgeDriver({
|
|
11714
|
-
sessionId:
|
|
11865
|
+
sessionId: session2.sessionId,
|
|
11715
11866
|
platform: platform3,
|
|
11716
|
-
appId:
|
|
11717
|
-
deviceId:
|
|
11867
|
+
appId: session2.appId,
|
|
11868
|
+
deviceId: session2.deviceId,
|
|
11718
11869
|
// Deterministic-state authorization: Android emulators / iOS
|
|
11719
11870
|
// simulators only (VALIDATEQA_MOBILE_APP_RESET overrides the
|
|
11720
11871
|
// Android default) — a personal physical device must never have
|
|
11721
11872
|
// its app data wiped.
|
|
11722
|
-
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3,
|
|
11873
|
+
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session2.deviceId),
|
|
11723
11874
|
// Dual-device discovery + long AI turns can lose WDA/UIA2 more than
|
|
11724
11875
|
// once per run. One recovery was too tight — the next snapshot
|
|
11725
11876
|
// failed as "no UI tree" after a single WDA death.
|
|
@@ -11728,13 +11879,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11728
11879
|
if (discoveryTeardownStarted) {
|
|
11729
11880
|
throw new Error("discovery teardown already started \u2014 not recreating the Appium session");
|
|
11730
11881
|
}
|
|
11731
|
-
const lost =
|
|
11882
|
+
const lost = session2;
|
|
11732
11883
|
mobileOnLog(
|
|
11733
11884
|
`[appium] discovery session lost (${cause.message}); recreating on the leased device`
|
|
11734
11885
|
);
|
|
11735
11886
|
if (lost) {
|
|
11736
11887
|
detachMirrorSession(lost.deviceId);
|
|
11737
|
-
|
|
11888
|
+
session2 = null;
|
|
11738
11889
|
try {
|
|
11739
11890
|
await lost.stop();
|
|
11740
11891
|
} catch {
|
|
@@ -11748,7 +11899,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11748
11899
|
}
|
|
11749
11900
|
throw new Error("discovery teardown started during session recovery \u2014 replacement session closed");
|
|
11750
11901
|
}
|
|
11751
|
-
|
|
11902
|
+
session2 = replacement;
|
|
11752
11903
|
attachMirrorWithRelay(replacement.sessionId, platform3, replacement.deviceId);
|
|
11753
11904
|
liveBrowserHandle.patch({
|
|
11754
11905
|
deviceId: replacement.deviceId,
|
|
@@ -11812,7 +11963,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11812
11963
|
const apiCalls = stopped;
|
|
11813
11964
|
const captureHealth = activeCapture.health();
|
|
11814
11965
|
if (discoveryResult) {
|
|
11815
|
-
discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult,
|
|
11966
|
+
discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult, session2?.appId ?? mobileTarget.appId) : void 0;
|
|
11816
11967
|
const priorHealth = discoveryResult.healthObservations;
|
|
11817
11968
|
const healthObject = {
|
|
11818
11969
|
consoleErrors: priorHealth?.consoleErrors ?? [],
|
|
@@ -11827,9 +11978,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11827
11978
|
mobileOnLog(`[mobile-net] capture stop failed: ${capErr instanceof Error ? capErr.message : String(capErr)}`);
|
|
11828
11979
|
}
|
|
11829
11980
|
}
|
|
11830
|
-
if (
|
|
11831
|
-
detachMirrorSession(
|
|
11832
|
-
const closing =
|
|
11981
|
+
if (session2) {
|
|
11982
|
+
detachMirrorSession(session2.deviceId);
|
|
11983
|
+
const closing = session2;
|
|
11833
11984
|
try {
|
|
11834
11985
|
const stopped = await timeboxed(closing.stop());
|
|
11835
11986
|
if (stopped === TEARDOWN_STEP_TIMEOUT) {
|
|
@@ -11902,7 +12053,7 @@ ${line}`)
|
|
|
11902
12053
|
};
|
|
11903
12054
|
} else if (ctx.frameworkImport?.kind === "app-map") {
|
|
11904
12055
|
log.info("Building the app map.");
|
|
11905
|
-
discoveryResult = await (0,
|
|
12056
|
+
discoveryResult = await (0, import_runner_core10.runAppMapOnRunner)(ctx, { onLog: (line) => log.info(line) });
|
|
11906
12057
|
} else if (ctx.frameworkImport?.kind === "repo-enrichment") {
|
|
11907
12058
|
const frameworkImport = ctx.frameworkImport;
|
|
11908
12059
|
const primaryCli = isRepositoryCli(frameworkImport.repoHuntCli) ? frameworkImport.repoHuntCli : "claude-code";
|
|
@@ -11927,7 +12078,7 @@ ${line}`)
|
|
|
11927
12078
|
})
|
|
11928
12079
|
});
|
|
11929
12080
|
} else {
|
|
11930
|
-
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ?
|
|
12081
|
+
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ? import_runner_core10.runRepoAnalysisOnRunner : import_framework_import_runner.runFrameworkImportOnRunner;
|
|
11931
12082
|
discoveryResult = await runRepoImport(ctx, { onTestGenerated, acquireLoginBrowser, onAudit: discoveryAudit }, {
|
|
11932
12083
|
archiveUrl: ctx.frameworkImport?.source === "upload" ? `${opts.serverUrl}/api/runner/runs/${run2.runId}/import-archive` : void 0,
|
|
11933
12084
|
archiveAuthHeader: headers.Authorization,
|
|
@@ -11989,7 +12140,7 @@ ${line}`)
|
|
|
11989
12140
|
}
|
|
11990
12141
|
const resolveCliSubscription = (engine, logLine) => (0, import_fetch_cli_subscription.fetchCliSubscription)({ serverUrl: opts.serverUrl, runId: run2.runId, headers, cli: engine, onLog: logLine });
|
|
11991
12142
|
const claimNonce = claimNonceFor(run2.runId);
|
|
11992
|
-
savedSignIn = ["survey", "deep", "full"].includes(ctx.mode) && claimNonce && typeof run2.claimEpoch === "number" ? await (0,
|
|
12143
|
+
savedSignIn = ["survey", "deep", "full"].includes(ctx.mode) && claimNonce && typeof run2.claimEpoch === "number" ? await (0, import_runner_core10.openSavedSignInSession)((0, import_runner_core10.createSavedSignInClient)({
|
|
11993
12144
|
serverUrl: opts.serverUrl,
|
|
11994
12145
|
runId: run2.runId,
|
|
11995
12146
|
claimEpoch: run2.claimEpoch,
|
|
@@ -12009,7 +12160,7 @@ ${line}`)
|
|
|
12009
12160
|
if (ctx.repoBrief) discoveryResult.repoBrief = ctx.repoBrief;
|
|
12010
12161
|
if (ctx.repoRouteManifest) discoveryResult.repoRouteManifest = ctx.repoRouteManifest;
|
|
12011
12162
|
}
|
|
12012
|
-
discoveryResult = (0,
|
|
12163
|
+
discoveryResult = (0, import_runner_core10.redactDiscoveryCaptureNeedles)(discoveryResult, {
|
|
12013
12164
|
...run2.testVariables,
|
|
12014
12165
|
// The saved sign-in's values, with every one it rotated to during the run.
|
|
12015
12166
|
...savedSignIn?.loaded ? savedSignIn.secrets() : {}
|
|
@@ -12167,7 +12318,7 @@ ${line}`)
|
|
|
12167
12318
|
completionTokens: info.completionTokens,
|
|
12168
12319
|
totalTokens: info.totalTokens,
|
|
12169
12320
|
...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
|
|
12170
|
-
metadata: { groupId: info.groupId, iteration: info.iteration, phase: "capture", featureName: ctx.featureName, ...(0,
|
|
12321
|
+
metadata: { groupId: info.groupId, iteration: info.iteration, phase: "capture", featureName: ctx.featureName, ...(0, import_runner_core10.cliAuditMetadata)(info) }
|
|
12171
12322
|
});
|
|
12172
12323
|
};
|
|
12173
12324
|
const logBuffer = [];
|
|
@@ -12384,7 +12535,7 @@ ${line}`)
|
|
|
12384
12535
|
completionTokens: info.completionTokens,
|
|
12385
12536
|
totalTokens: info.totalTokens,
|
|
12386
12537
|
...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
|
|
12387
|
-
metadata: { groupId: info.groupId, iteration: info.iteration, testName: run2.testCaseId, platform: "mobile", ...(0,
|
|
12538
|
+
metadata: { groupId: info.groupId, iteration: info.iteration, testName: run2.testCaseId, platform: "mobile", ...(0, import_runner_core10.cliAuditMetadata)(info) }
|
|
12388
12539
|
});
|
|
12389
12540
|
},
|
|
12390
12541
|
runLiveHeal: async (a) => {
|
|
@@ -12726,7 +12877,7 @@ ${result2.logs}`);
|
|
|
12726
12877
|
if (storageStatePath) {
|
|
12727
12878
|
log.info(`Using auth state for project ${projectId}`);
|
|
12728
12879
|
}
|
|
12729
|
-
const authSession = (0,
|
|
12880
|
+
const authSession = (0, import_runner_core9.sharedAuthSessionStamp)(storageStatePath, captureAuth, sharedAuthInjectEnabled());
|
|
12730
12881
|
const runAuthPath = captureAuth ? authPaths?.capturePath : void 0;
|
|
12731
12882
|
const result = await (async () => {
|
|
12732
12883
|
try {
|
|
@@ -12785,16 +12936,17 @@ ${result2.logs}`);
|
|
|
12785
12936
|
runStatus = "ERROR";
|
|
12786
12937
|
log.warn(`\u26A0 CONFIG_ISSUE from native execution - posting ERROR (not FAILED) to skip heal/quarantine`);
|
|
12787
12938
|
}
|
|
12788
|
-
const authRelabel = (0,
|
|
12939
|
+
const authRelabel = (0, import_runner_core9.authReadinessRelabel)({
|
|
12789
12940
|
status: runStatus,
|
|
12790
12941
|
error: runError,
|
|
12791
12942
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
12792
12943
|
tags: run2.tags ?? [],
|
|
12944
|
+
code: run2.playwrightCode,
|
|
12793
12945
|
logs: result.logs,
|
|
12794
12946
|
stepResults: result.stepResults,
|
|
12795
12947
|
apiCalls: result.apiCalls,
|
|
12796
12948
|
authStateExpected,
|
|
12797
|
-
authStateInjected: (0,
|
|
12949
|
+
authStateInjected: (0, import_runner_core9.storageStateDelivered)(storageStatePath, result),
|
|
12798
12950
|
needsStandingHeaders: normalRunAuthPlan.needsStandingHeaders,
|
|
12799
12951
|
standingHeadersInjected: !!result.authDelivery?.standingHeadersInjected
|
|
12800
12952
|
});
|
|
@@ -12803,7 +12955,7 @@ ${result2.logs}`);
|
|
|
12803
12955
|
runError = authRelabel.error;
|
|
12804
12956
|
log.warn(authRelabel.logLine);
|
|
12805
12957
|
}
|
|
12806
|
-
(0,
|
|
12958
|
+
(0, import_runner_core10.throwIfRunAborted)();
|
|
12807
12959
|
await postRunResult({
|
|
12808
12960
|
status: runStatus,
|
|
12809
12961
|
stepResults: result.stepResults,
|
|
@@ -12826,7 +12978,7 @@ ${result2.logs}`);
|
|
|
12826
12978
|
if (result.passed) {
|
|
12827
12979
|
log.ok(`PASSED (${duration}ms)${result.suspiciousPass ? ` \u26A0 suspicious-pass (viewport ${Math.round(result.suspiciousPass.emptyRatio * 100)}% blank)` : ""}`);
|
|
12828
12980
|
} else if (runStatus === "ERROR") {
|
|
12829
|
-
log.warn(
|
|
12981
|
+
log.warn(blockedRunLogLine(runError, duration));
|
|
12830
12982
|
} else {
|
|
12831
12983
|
log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
|
|
12832
12984
|
if (result.logs) {
|
|
@@ -12837,14 +12989,14 @@ ${result2.logs}`);
|
|
|
12837
12989
|
}
|
|
12838
12990
|
} catch (err) {
|
|
12839
12991
|
if (err instanceof RunNoLongerActiveError) throw err;
|
|
12840
|
-
const abortOutcome = (0,
|
|
12992
|
+
const abortOutcome = (0, import_runner_core10.runAbortOutcome)(err, signal);
|
|
12841
12993
|
if (abortOutcome === "discard") {
|
|
12842
|
-
throw new RunNoLongerActiveError((0,
|
|
12994
|
+
throw new RunNoLongerActiveError((0, import_runner_core10.isSupersededRunAbort)(signal) ? `Run ${run2.runId} was superseded by a newer claim` : `Run ${run2.runId} was cancelled on the server`);
|
|
12843
12995
|
}
|
|
12844
12996
|
if (abortOutcome === "interrupt") {
|
|
12845
12997
|
await postRunResult({
|
|
12846
12998
|
status: "ERROR",
|
|
12847
|
-
error:
|
|
12999
|
+
error: import_runner_core10.RUNNER_SHUTDOWN_INTERRUPT_ERROR,
|
|
12848
13000
|
duration: Date.now() - startTime
|
|
12849
13001
|
});
|
|
12850
13002
|
throw err;
|
|
@@ -12961,13 +13113,6 @@ async function startRunner(opts) {
|
|
|
12961
13113
|
6e4,
|
|
12962
13114
|
envInt("VALIDATE_RUNNER_VERSION_CHECK_MS", 15 * 6e4)
|
|
12963
13115
|
);
|
|
12964
|
-
const exitForUpgrade = (latestVersion, installed) => {
|
|
12965
|
-
console.error(formatUpgradeInstructions(latestVersion));
|
|
12966
|
-
console.error(` Installed: ${installed}`);
|
|
12967
|
-
console.error(` Required: ${latestVersion} (or newer)`);
|
|
12968
|
-
console.error("");
|
|
12969
|
-
process.exit(1);
|
|
12970
|
-
};
|
|
12971
13116
|
let drainingForUpgrade = false;
|
|
12972
13117
|
const deferUpgradeWhileBusy = () => {
|
|
12973
13118
|
if (upgradeAction(activeRuns) === "exit") return false;
|
|
@@ -13048,9 +13193,11 @@ async function startRunner(opts) {
|
|
|
13048
13193
|
await browserPool.init();
|
|
13049
13194
|
log.ok(`Browser pool: ${poolSize} shared browsers ready`);
|
|
13050
13195
|
} catch (err) {
|
|
13051
|
-
|
|
13052
|
-
log.fail(
|
|
13053
|
-
|
|
13196
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
13197
|
+
log.fail(`Browser pool failed to init: ${message}`);
|
|
13198
|
+
const failure = browserPoolInitFailure(message, bundledPlaywrightVersion2());
|
|
13199
|
+
log.fail(failure.advice);
|
|
13200
|
+
process.exit(failure.exitCode);
|
|
13054
13201
|
}
|
|
13055
13202
|
}
|
|
13056
13203
|
const SHUTDOWN_FINISH_MS = Math.max(0, envInt("RUNNER_SHUTDOWN_FINISH_MS", 5e3));
|
|
@@ -13322,7 +13469,7 @@ async function startRunner(opts) {
|
|
|
13322
13469
|
}
|
|
13323
13470
|
});
|
|
13324
13471
|
};
|
|
13325
|
-
const removeSupersededRunFetchFence = (0,
|
|
13472
|
+
const removeSupersededRunFetchFence = (0, import_runner_core10.installSupersededRunFetchFence)(serverUrl);
|
|
13326
13473
|
const heartbeat = setInterval(sendHeartbeat, 3e4);
|
|
13327
13474
|
sendHeartbeat();
|
|
13328
13475
|
let shuttingDown = false;
|
|
@@ -13334,7 +13481,7 @@ async function startRunner(opts) {
|
|
|
13334
13481
|
headers: { ...headers, "Content-Type": "application/json" },
|
|
13335
13482
|
body: JSON.stringify(withClaimEpoch({
|
|
13336
13483
|
status: "ERROR",
|
|
13337
|
-
error:
|
|
13484
|
+
error: import_runner_core10.RUNNER_SHUTDOWN_INTERRUPT_ERROR
|
|
13338
13485
|
}, claimEpoch)),
|
|
13339
13486
|
signal: AbortSignal.timeout(5e3)
|
|
13340
13487
|
}));
|
|
@@ -13350,7 +13497,7 @@ async function startRunner(opts) {
|
|
|
13350
13497
|
setTimeout(() => {
|
|
13351
13498
|
console.error("Shutdown grace elapsed \u2014 forcing exit");
|
|
13352
13499
|
process.exit(0);
|
|
13353
|
-
}, (0,
|
|
13500
|
+
}, (0, import_runner_core10.shutdownForceExitMs)(SHUTDOWN_FINISH_MS, SHUTDOWN_GRACE_MS)).unref();
|
|
13354
13501
|
clearInterval(heartbeat);
|
|
13355
13502
|
heartbeatKeepalive.stop();
|
|
13356
13503
|
if (liveBrowserHeartbeatTimer) {
|
|
@@ -13368,7 +13515,7 @@ async function startRunner(opts) {
|
|
|
13368
13515
|
if (claimPending) {
|
|
13369
13516
|
await Promise.race([claimPending, new Promise((resolve3) => setTimeout(resolve3, SHUTDOWN_FINISH_MS))]);
|
|
13370
13517
|
}
|
|
13371
|
-
await (0,
|
|
13518
|
+
await (0, import_runner_core10.drainAbortedRuns)({
|
|
13372
13519
|
finishMs: SHUTDOWN_FINISH_MS,
|
|
13373
13520
|
graceMs: SHUTDOWN_GRACE_MS,
|
|
13374
13521
|
inFlight: activeWebRuns,
|
|
@@ -13385,7 +13532,7 @@ async function startRunner(opts) {
|
|
|
13385
13532
|
await stopAppiumServer();
|
|
13386
13533
|
}
|
|
13387
13534
|
await browserPool.shutdown();
|
|
13388
|
-
await (0,
|
|
13535
|
+
await (0, import_runner_core11.closeLocalGoogleBroker)().catch(() => {
|
|
13389
13536
|
});
|
|
13390
13537
|
removeSupersededRunFetchFence();
|
|
13391
13538
|
log.info("Runner stopped.");
|
|
@@ -13449,7 +13596,7 @@ async function startRunner(opts) {
|
|
|
13449
13596
|
const abortSupersededRun = (runId, status, serverEpoch) => {
|
|
13450
13597
|
const controller = activeWebRuns.get(runId);
|
|
13451
13598
|
const already = !controller || controller.signal.aborted;
|
|
13452
|
-
if (controller) (0,
|
|
13599
|
+
if (controller) (0, import_runner_core10.abortRun)(controller, "superseded", runId);
|
|
13453
13600
|
activeMobileRuns.get(runId)?.triggerAbort(`was superseded by a newer claim (epoch ${serverEpoch ?? "?"}, status ${status})`);
|
|
13454
13601
|
if (!already) {
|
|
13455
13602
|
log.warn(`Run ${runId} was superseded by a newer claim (server epoch ${serverEpoch ?? "?"}, ours ${claimedRunEpochs.get(runId) ?? "?"}) \u2014 stopping it; this runner posts nothing more for it.`);
|
|
@@ -13465,7 +13612,7 @@ async function startRunner(opts) {
|
|
|
13465
13612
|
(runId, status) => {
|
|
13466
13613
|
const controller = activeWebRuns.get(runId);
|
|
13467
13614
|
const already = !controller || controller.signal.aborted;
|
|
13468
|
-
if (controller) (0,
|
|
13615
|
+
if (controller) (0, import_runner_core10.abortRun)(controller, "cancelled", runId);
|
|
13469
13616
|
activeMobileRuns.get(runId)?.triggerAbort(`was cancelled server-side (status ${status})`);
|
|
13470
13617
|
if (!already) log.warn(`Run ${runId} was cancelled server-side (status ${status}) \u2014 stopping it.`);
|
|
13471
13618
|
}
|
|
@@ -13665,6 +13812,7 @@ async function startRunner(opts) {
|
|
|
13665
13812
|
const applyRelease = () => {
|
|
13666
13813
|
claimedRunEpochs.delete(run2.runId);
|
|
13667
13814
|
claimNonces.delete(run2.runId);
|
|
13815
|
+
if (activeWebRuns.get(run2.runId) === runAbort) activeWebRuns.delete(run2.runId);
|
|
13668
13816
|
activeRuns--;
|
|
13669
13817
|
decrementType(runMode);
|
|
13670
13818
|
decrementProject(runProject);
|
|
@@ -13723,9 +13871,9 @@ async function startRunner(opts) {
|
|
|
13723
13871
|
runWatchdog.start();
|
|
13724
13872
|
}
|
|
13725
13873
|
activeWebRuns.set(run2.runId, runAbort);
|
|
13726
|
-
const runWritePolicy = (0,
|
|
13727
|
-
const runSignInSecrets = runWritePolicy ? (0,
|
|
13728
|
-
(0,
|
|
13874
|
+
const runWritePolicy = (0, import_runner_core10.parseRunWritePolicy)(run2.runWritePolicy);
|
|
13875
|
+
const runSignInSecrets = runWritePolicy ? (0, import_runner_core10.signInSecretsOf)({ ...run2.discoveryContext?.variableContext?.allTestVariables ?? {}, ...run2.testVariables ?? {} }) : [];
|
|
13876
|
+
(0, import_runner_core10.withRunWritePolicyScope)(runWritePolicy, () => (0, import_runner_core11.withLocalGoogleScope)({ baseUrl: run2.baseUrl, runId: run2.runId }, () => executeRun(run2, opts, headers, requestLiveBrowserHeartbeat, (testCaseId) => {
|
|
13729
13877
|
if (mobilePlatform) {
|
|
13730
13878
|
mobileHealHold.set(testCaseId, { expiry: Date.now() + MOBILE_HEAL_HOLD_MS, platform: mobilePlatform });
|
|
13731
13879
|
}
|
|
@@ -13733,11 +13881,11 @@ async function startRunner(opts) {
|
|
|
13733
13881
|
runsCompleted++;
|
|
13734
13882
|
}).catch((err) => {
|
|
13735
13883
|
const abortReason = runAbort.signal.reason;
|
|
13736
|
-
if ((0,
|
|
13884
|
+
if ((0, import_runner_core10.isRunAbortError)(abortReason) && abortReason.reason === "shutdown") {
|
|
13737
13885
|
log.info(`Run ${run2.runId} was interrupted by runner shutdown.`);
|
|
13738
13886
|
return;
|
|
13739
13887
|
}
|
|
13740
|
-
if ((0,
|
|
13888
|
+
if ((0, import_runner_core10.isSupersededRunAbort)(runAbort.signal)) {
|
|
13741
13889
|
log.info(`Run ${run2.runId} was superseded by a newer claim; this runner posted nothing more for it.`);
|
|
13742
13890
|
return;
|
|
13743
13891
|
}
|
|
@@ -13749,7 +13897,7 @@ async function startRunner(opts) {
|
|
|
13749
13897
|
log.warn(`Run ${run2.runId} crashed: ${msg}`);
|
|
13750
13898
|
}).finally(() => {
|
|
13751
13899
|
runSettled = true;
|
|
13752
|
-
activeWebRuns.delete(run2.runId);
|
|
13900
|
+
if (activeWebRuns.get(run2.runId) === runAbort) activeWebRuns.delete(run2.runId);
|
|
13753
13901
|
if (runWatchdog) runWatchdog.settle();
|
|
13754
13902
|
else applyRelease();
|
|
13755
13903
|
});
|
|
@@ -13809,12 +13957,61 @@ async function startRunner(opts) {
|
|
|
13809
13957
|
}
|
|
13810
13958
|
}
|
|
13811
13959
|
|
|
13960
|
+
// src/resolve-credentials.ts
|
|
13961
|
+
function sameServer(a, b) {
|
|
13962
|
+
const norm = (u) => u.trim().replace(/\/+$/, "").toLowerCase();
|
|
13963
|
+
return norm(a) === norm(b);
|
|
13964
|
+
}
|
|
13965
|
+
function resolveCredentials(flags, env, saved) {
|
|
13966
|
+
const rawServer = flags.server || env.SERVER_URL || saved?.serverUrl;
|
|
13967
|
+
const serverUrl = rawServer?.replace(/\/+$/, "");
|
|
13968
|
+
const savedApplies = saved !== null && serverUrl !== void 0 && sameServer(serverUrl, saved.serverUrl);
|
|
13969
|
+
return {
|
|
13970
|
+
savedLoginSkipped: saved !== null && !savedApplies,
|
|
13971
|
+
authToken: flags.token || env.AUTH_TOKEN || (savedApplies ? saved.token : void 0),
|
|
13972
|
+
serverUrl,
|
|
13973
|
+
projectId: flags.project || env.PROJECT_ID || (savedApplies ? saved.projectId : void 0)
|
|
13974
|
+
};
|
|
13975
|
+
}
|
|
13976
|
+
|
|
13812
13977
|
// src/login-check.ts
|
|
13813
13978
|
var RUNNER_TOKEN_PREFIXES = ["urt_", "rt_"];
|
|
13814
13979
|
function runnerTokenShapeProblem(token) {
|
|
13815
13980
|
if (RUNNER_TOKEN_PREFIXES.some((prefix) => token.startsWith(prefix))) return null;
|
|
13816
13981
|
return "This is not a runner token. Use the runner token (urt_...) from Project Settings \u2192 Local Runner; an API token (pat_...) or a login token cannot run tests.";
|
|
13817
13982
|
}
|
|
13983
|
+
var SERVER_URL_HINT = "Use the server URL shown in Project Settings \u2192 Local Runner (the dashboard address is not always it).";
|
|
13984
|
+
function loginServerUrlProblem(serverUrl, label = "--server") {
|
|
13985
|
+
let parsed = null;
|
|
13986
|
+
try {
|
|
13987
|
+
parsed = new URL(serverUrl);
|
|
13988
|
+
} catch {
|
|
13989
|
+
parsed = null;
|
|
13990
|
+
}
|
|
13991
|
+
if (parsed && (parsed.protocol === "https:" || parsed.protocol === "http:")) return null;
|
|
13992
|
+
const guess = !parsed && serverUrl && !serverUrl.includes("://") ? ` (did you mean https://${serverUrl}?)` : "";
|
|
13993
|
+
return `${label} needs a full address starting with https:// or http://, got '${serverUrl}'${guess}.`;
|
|
13994
|
+
}
|
|
13995
|
+
function sendsTokenInPlaintext(serverUrl) {
|
|
13996
|
+
let url;
|
|
13997
|
+
try {
|
|
13998
|
+
url = new URL(serverUrl);
|
|
13999
|
+
} catch {
|
|
14000
|
+
return false;
|
|
14001
|
+
}
|
|
14002
|
+
if (url.protocol !== "http:") return false;
|
|
14003
|
+
const host = url.hostname.toLowerCase();
|
|
14004
|
+
const loopback = host === "localhost" || host.endsWith(".localhost") || host === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(host);
|
|
14005
|
+
return !loopback;
|
|
14006
|
+
}
|
|
14007
|
+
async function isRunnerHealthAnswer(res) {
|
|
14008
|
+
try {
|
|
14009
|
+
const body = await res.json();
|
|
14010
|
+
return typeof body === "object" && body !== null && !Array.isArray(body) && typeof body.status === "string";
|
|
14011
|
+
} catch {
|
|
14012
|
+
return false;
|
|
14013
|
+
}
|
|
14014
|
+
}
|
|
13818
14015
|
async function checkRunnerLogin(serverUrl, token, fetchImpl = fetch) {
|
|
13819
14016
|
let res;
|
|
13820
14017
|
try {
|
|
@@ -13822,17 +14019,388 @@ async function checkRunnerLogin(serverUrl, token, fetchImpl = fetch) {
|
|
|
13822
14019
|
} catch {
|
|
13823
14020
|
return { ok: false, message: `Could not reach ${serverUrl}` };
|
|
13824
14021
|
}
|
|
13825
|
-
if (res.ok)
|
|
14022
|
+
if (res.ok) {
|
|
14023
|
+
if (await isRunnerHealthAnswer(res)) return { ok: true };
|
|
14024
|
+
return { ok: false, wrongServer: true, message: `${serverUrl} is not a validate.qa server: it answered, but not as the runner API - credentials not saved. Check --server. ${SERVER_URL_HINT}` };
|
|
14025
|
+
}
|
|
13826
14026
|
if (res.status === 401 || res.status === 403) {
|
|
13827
14027
|
return { ok: false, message: "Invalid token - check your runner token (it may be revoked, or belong to another project)" };
|
|
13828
14028
|
}
|
|
14029
|
+
if (res.status === 404 || res.status === 405) {
|
|
14030
|
+
return { ok: false, wrongServer: true, message: `${serverUrl} is not a validate.qa server (HTTP ${res.status} for the runner API) - credentials not saved. Check --server. ${SERVER_URL_HINT}` };
|
|
14031
|
+
}
|
|
13829
14032
|
return { ok: false, message: `Server returned ${res.status} - credentials not saved, try again later` };
|
|
13830
14033
|
}
|
|
14034
|
+
function startServerProblem(check2, serverUrl) {
|
|
14035
|
+
if (check2.ok || !check2.wrongServer) return null;
|
|
14036
|
+
return `${serverUrl} is not a validate.qa server: it has no runner API, so the runner would poll it forever. Check --server, SERVER_URL or the saved login. ${SERVER_URL_HINT}`;
|
|
14037
|
+
}
|
|
14038
|
+
var MAX_TOKEN_INPUT = 4096;
|
|
14039
|
+
async function resolveLoginToken(input) {
|
|
14040
|
+
if (input.flag !== void 0 && input.flag !== "-") return { token: input.flag };
|
|
14041
|
+
const raw = input.stdinIsTTY ? await input.promptHidden("Paste the runner token (urt_..., not shown) and press Enter: ") : await input.readPiped();
|
|
14042
|
+
const token = raw.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
|
|
14043
|
+
if (!token) {
|
|
14044
|
+
return { error: "No runner token. Paste it at the prompt, pipe it on stdin, or pass --token <urt_...>." };
|
|
14045
|
+
}
|
|
14046
|
+
return { token };
|
|
14047
|
+
}
|
|
14048
|
+
function readPipedToken(stream, max = MAX_TOKEN_INPUT) {
|
|
14049
|
+
return new Promise((resolve3, reject) => {
|
|
14050
|
+
let text = "";
|
|
14051
|
+
const cleanup = () => {
|
|
14052
|
+
stream.removeListener("data", onData);
|
|
14053
|
+
stream.removeListener("end", onEnd);
|
|
14054
|
+
stream.removeListener("error", onError);
|
|
14055
|
+
};
|
|
14056
|
+
const onData = (chunk) => {
|
|
14057
|
+
text += String(chunk);
|
|
14058
|
+
if (text.length > max) {
|
|
14059
|
+
cleanup();
|
|
14060
|
+
stream.pause();
|
|
14061
|
+
reject(new Error(`more than ${max} characters on stdin: that is not a runner token`));
|
|
14062
|
+
}
|
|
14063
|
+
};
|
|
14064
|
+
const onEnd = () => {
|
|
14065
|
+
cleanup();
|
|
14066
|
+
resolve3(text);
|
|
14067
|
+
};
|
|
14068
|
+
const onError = (err) => {
|
|
14069
|
+
cleanup();
|
|
14070
|
+
reject(err);
|
|
14071
|
+
};
|
|
14072
|
+
stream.on("data", onData);
|
|
14073
|
+
stream.on("end", onEnd);
|
|
14074
|
+
stream.on("error", onError);
|
|
14075
|
+
stream.resume();
|
|
14076
|
+
});
|
|
14077
|
+
}
|
|
14078
|
+
function promptHidden(input, output, question) {
|
|
14079
|
+
return new Promise((resolve3, reject) => {
|
|
14080
|
+
const wasRaw = input.isRaw === true;
|
|
14081
|
+
let line = "";
|
|
14082
|
+
output.write(question);
|
|
14083
|
+
input.setRawMode?.(true);
|
|
14084
|
+
const finish = (err) => {
|
|
14085
|
+
input.removeListener("data", onData);
|
|
14086
|
+
input.setRawMode?.(wasRaw);
|
|
14087
|
+
input.pause();
|
|
14088
|
+
output.write("\n");
|
|
14089
|
+
if (err) reject(err);
|
|
14090
|
+
else resolve3(line);
|
|
14091
|
+
};
|
|
14092
|
+
const onData = (chunk) => {
|
|
14093
|
+
for (const ch of String(chunk)) {
|
|
14094
|
+
if (ch === "\r" || ch === "\n" || ch === "") {
|
|
14095
|
+
finish();
|
|
14096
|
+
return;
|
|
14097
|
+
}
|
|
14098
|
+
if (ch === "") {
|
|
14099
|
+
finish(new Error("cancelled"));
|
|
14100
|
+
return;
|
|
14101
|
+
}
|
|
14102
|
+
if (ch === "\x7F" || ch === "\b") {
|
|
14103
|
+
line = line.slice(0, -1);
|
|
14104
|
+
continue;
|
|
14105
|
+
}
|
|
14106
|
+
if (line.length >= MAX_TOKEN_INPUT) {
|
|
14107
|
+
finish(new Error(`more than ${MAX_TOKEN_INPUT} characters: that is not a runner token`));
|
|
14108
|
+
return;
|
|
14109
|
+
}
|
|
14110
|
+
line += ch;
|
|
14111
|
+
}
|
|
14112
|
+
};
|
|
14113
|
+
input.on("data", onData);
|
|
14114
|
+
input.resume();
|
|
14115
|
+
});
|
|
14116
|
+
}
|
|
14117
|
+
|
|
14118
|
+
// src/status-check.ts
|
|
14119
|
+
var SAVED = "saved credentials";
|
|
14120
|
+
function pick(flag, flagName, envValue, envName, savedValue) {
|
|
14121
|
+
if (flag) return { value: flag, from: flagName };
|
|
14122
|
+
if (envValue) return { value: envValue, from: envName };
|
|
14123
|
+
if (savedValue) return { value: savedValue, from: SAVED };
|
|
14124
|
+
return null;
|
|
14125
|
+
}
|
|
14126
|
+
function resolveStatusCredentials(opts, env, saved) {
|
|
14127
|
+
const server2 = pick(opts.server, "--server", env.SERVER_URL, "SERVER_URL", saved?.serverUrl);
|
|
14128
|
+
const norm = (u) => u.trim().replace(/\/+$/, "").toLowerCase();
|
|
14129
|
+
const savedApplies = !!saved && !!server2 && norm(server2.value) === norm(saved.serverUrl);
|
|
14130
|
+
const token = pick(opts.token, "--token", env.AUTH_TOKEN, "AUTH_TOKEN", savedApplies ? saved?.token : void 0);
|
|
14131
|
+
const project = pick(opts.project, "--project", env.PROJECT_ID, "PROJECT_ID", savedApplies ? saved?.projectId : void 0);
|
|
14132
|
+
if (!token && !server2) {
|
|
14133
|
+
return { missing: "No saved credentials. Run: validate-runner login, or set AUTH_TOKEN and SERVER_URL (or pass --token and --server)" };
|
|
14134
|
+
}
|
|
14135
|
+
if (!token && saved?.token && !savedApplies) {
|
|
14136
|
+
return { missing: `The saved login is for ${saved.serverUrl}, not ${server2?.value}. Run: validate-runner login --server <url>, or set AUTH_TOKEN (or pass --token)` };
|
|
14137
|
+
}
|
|
14138
|
+
if (!token) return { missing: "No runner token. Run: validate-runner login, or set AUTH_TOKEN (or pass --token)" };
|
|
14139
|
+
if (!server2) return { missing: "No server URL. Run: validate-runner login --server <url>, or set SERVER_URL (or pass --server)" };
|
|
14140
|
+
return {
|
|
14141
|
+
serverUrl: server2.value.replace(/\/+$/, ""),
|
|
14142
|
+
token: token.value,
|
|
14143
|
+
...project ? { projectId: project.value } : {},
|
|
14144
|
+
from: { serverUrl: server2.from, token: token.from, ...project ? { projectId: project.from } : {} }
|
|
14145
|
+
};
|
|
14146
|
+
}
|
|
14147
|
+
var STATUS_OK = 0;
|
|
14148
|
+
var STATUS_FAILED = 1;
|
|
14149
|
+
async function runStatusCheck(resolved, fetchImpl = fetch, out = console) {
|
|
14150
|
+
if (!resolved || "missing" in resolved) {
|
|
14151
|
+
out.error(resolved ? resolved.missing : "No saved credentials. Run: validate-runner login");
|
|
14152
|
+
return STATUS_FAILED;
|
|
14153
|
+
}
|
|
14154
|
+
const saved = resolved;
|
|
14155
|
+
const from = (source) => source ? ` (from ${source})` : "";
|
|
14156
|
+
out.log(`Server: ${saved.serverUrl}${from(saved.from?.serverUrl)}`);
|
|
14157
|
+
out.log(`Project: ${saved.projectId || "(not set)"}${from(saved.projectId ? saved.from?.projectId : void 0)}`);
|
|
14158
|
+
out.log(`Token: ${saved.token.slice(0, 8)}...${saved.token.slice(-4)}${from(saved.from?.token)}`);
|
|
14159
|
+
out.log("");
|
|
14160
|
+
let res;
|
|
14161
|
+
try {
|
|
14162
|
+
res = await fetchImpl(`${saved.serverUrl}/api/runner/health`, {
|
|
14163
|
+
headers: {
|
|
14164
|
+
Authorization: `Bearer ${saved.token}`,
|
|
14165
|
+
...saved.projectId ? { "X-Project-Id": saved.projectId } : {}
|
|
14166
|
+
}
|
|
14167
|
+
});
|
|
14168
|
+
} catch {
|
|
14169
|
+
out.error("\u2717 Could not reach server");
|
|
14170
|
+
return STATUS_FAILED;
|
|
14171
|
+
}
|
|
14172
|
+
if (res.status === 401 || res.status === 403) {
|
|
14173
|
+
out.error(`\u2717 Server returned ${res.status}: invalid token - check your runner token (it may be revoked, or belong to another project). Run: validate-runner login`);
|
|
14174
|
+
return STATUS_FAILED;
|
|
14175
|
+
}
|
|
14176
|
+
if (!res.ok) {
|
|
14177
|
+
out.error(`\u2717 Server returned ${res.status}`);
|
|
14178
|
+
return STATUS_FAILED;
|
|
14179
|
+
}
|
|
14180
|
+
let data = null;
|
|
14181
|
+
try {
|
|
14182
|
+
data = await res.json();
|
|
14183
|
+
} catch {
|
|
14184
|
+
}
|
|
14185
|
+
if (!data || typeof data.status !== "string") {
|
|
14186
|
+
out.error(`\u2717 Server returned ${res.status} but not a runner health answer - check the server URL`);
|
|
14187
|
+
return STATUS_FAILED;
|
|
14188
|
+
}
|
|
14189
|
+
const runners = typeof data.runners === "number" ? data.runners : 0;
|
|
14190
|
+
out.log(`\u2713 Server reachable - runner status: ${data.status} (${runners} runner(s) online)`);
|
|
14191
|
+
return STATUS_OK;
|
|
14192
|
+
}
|
|
13831
14193
|
|
|
13832
14194
|
// src/services/sign-in-capture.ts
|
|
13833
14195
|
var import_browser_config3 = __toESM(require_browser_config());
|
|
13834
14196
|
var import_google_local_signin = __toESM(require_google_local_signin());
|
|
13835
14197
|
import { chromium } from "playwright";
|
|
14198
|
+
|
|
14199
|
+
// src/services/sign-in-detect.ts
|
|
14200
|
+
init_dist();
|
|
14201
|
+
var SIGN_IN_PATH_SEGMENT = /^(log-?in|log_in|sign-?in|sign_in|sign-?on|sign_on|auth|authenticate|authorize|oauth2?|openid|sso|saml2?|cas|callback|sign-?up|sign_up|register|password|forgot(-password)?|reset(-password)?|verify|mfa|2fa|otp|challenge|consent|signin-oidc|wp-login)$/i;
|
|
14202
|
+
var SIGN_IN_PATH = /\/sessions?\/new(\/|$)/i;
|
|
14203
|
+
var SIGN_OUT_PATH_SEGMENT = /^(log-?out|log_out|sign-?out|sign_out|sign-?off|log-?off|end-?session)$/i;
|
|
14204
|
+
var SIGN_IN_QUERY = /(^|&)((action|mode|view|page|step)=(log-?in|sign-?in|sign_in|register|sign-?up|lostpassword|rp|resetpass)(&|$)|(login|signin)=(fail(ed|ure)?|error|invalid|denied|required|expired|true|1)?(&|$))/i;
|
|
14205
|
+
var SIGN_OUT_PATH = /\/sessions?\/destroy(\/|$)/i;
|
|
14206
|
+
var SIGN_OUT_QUERY = /(^|&)(action|mode)=(log-?out|sign-?out|log-?off)(&|$)/i;
|
|
14207
|
+
var SIGN_IN_HOST_LABEL = /^(auth|login|signin|sso|accounts?|id|identity|idp|oauth|secure-?login)$/i;
|
|
14208
|
+
var AUTH_NAME = /token|auth|session|sess|jwt|sid|refresh|credential|bearer|access|login|user|remember/i;
|
|
14209
|
+
var FLOW_COOKIE = /csrf|xsrf|callback-?url|redirect|return-?to|state|nonce|pkce|code[-_]?verifier/i;
|
|
14210
|
+
var NOT_A_SIGN_IN_COOKIE = /^(_ga.*|_gid|_gat.*|_gcl_.*|_fbp|_fbc|_hj.*|_clck|_clsk|ajs_.*|amplitude.*|mp_.*|_mkto_trk|hubspotutk|__hs.*|__stripe_.*|intercom-.*|optimizely.*|_dd_s|_uetsid|_uetvid|_tt_.*|_pin_unauth|_rdt_uuid|cookieconsent.*|cookie_?consent.*|OptanonConsent|OptanonAlertBoxClosed|euconsent.*|NEXT_LOCALE|locale|lang|theme|cf_clearance|__cf_bm|_cfuvid|__cflb|datadome|_abck|bm_sz|bm_sv|ak_bmsc|reese84|incap_ses_.*|visid_incap_.*|nlbi_.*|_px.*|akavpau_.*|csrftoken|_csrf|XSRF-TOKEN|csrf_token|__Host-csrf.*)$/i;
|
|
14211
|
+
function bareHost(domain) {
|
|
14212
|
+
return domain.trim().toLowerCase().replace(/^\./, "").replace(/\.$/, "");
|
|
14213
|
+
}
|
|
14214
|
+
function onAppSite(hostOrDomain, target) {
|
|
14215
|
+
const host = bareHost(hostOrDomain);
|
|
14216
|
+
const app = target.hostname.toLowerCase();
|
|
14217
|
+
if (!host) return false;
|
|
14218
|
+
if (host === app) return true;
|
|
14219
|
+
const site = registrableDomain(app);
|
|
14220
|
+
return host === site || host.endsWith(`.${site}`);
|
|
14221
|
+
}
|
|
14222
|
+
function isSignInStep(pageUrl, target) {
|
|
14223
|
+
let url;
|
|
14224
|
+
try {
|
|
14225
|
+
url = new URL(pageUrl);
|
|
14226
|
+
} catch {
|
|
14227
|
+
return false;
|
|
14228
|
+
}
|
|
14229
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
14230
|
+
if (!onAppSite(url.hostname, target)) return true;
|
|
14231
|
+
const host = url.hostname.toLowerCase();
|
|
14232
|
+
if (host !== target.hostname.toLowerCase() && SIGN_IN_HOST_LABEL.test(host.split(".")[0] ?? "")) return true;
|
|
14233
|
+
if (SIGN_IN_QUERY.test(url.search.slice(1)) && !SIGN_OUT_QUERY.test(url.search.slice(1))) return true;
|
|
14234
|
+
if (SIGN_IN_PATH.test(url.pathname)) return true;
|
|
14235
|
+
return pathSegments(url).some((segment) => SIGN_IN_PATH_SEGMENT.test(segment));
|
|
14236
|
+
}
|
|
14237
|
+
function pathSegments(url) {
|
|
14238
|
+
const hashPath = /^#!?\//.test(url.hash) ? url.hash.replace(/^#!?/, "").split(/[?#]/)[0] ?? "" : "";
|
|
14239
|
+
return `${url.pathname}/${hashPath}`.split("/").map((segment) => segment.replace(/\.(php|html?|aspx?|jsp|cgi)$/i, ""));
|
|
14240
|
+
}
|
|
14241
|
+
function isSignOutPage(pageUrl) {
|
|
14242
|
+
try {
|
|
14243
|
+
const url = new URL(pageUrl);
|
|
14244
|
+
if (SIGN_OUT_PATH.test(url.pathname) || SIGN_OUT_QUERY.test(url.search.slice(1))) return true;
|
|
14245
|
+
return pathSegments(url).some((segment) => SIGN_OUT_PATH_SEGMENT.test(segment));
|
|
14246
|
+
} catch {
|
|
14247
|
+
return false;
|
|
14248
|
+
}
|
|
14249
|
+
}
|
|
14250
|
+
function isSignOutRequest(request, target) {
|
|
14251
|
+
if (/^(GET|HEAD|OPTIONS)$/i.test(request.method)) return false;
|
|
14252
|
+
let url;
|
|
14253
|
+
try {
|
|
14254
|
+
url = new URL(request.url);
|
|
14255
|
+
} catch {
|
|
14256
|
+
return false;
|
|
14257
|
+
}
|
|
14258
|
+
if (!onAppSite(url.hostname, target)) return false;
|
|
14259
|
+
if (isSignOutPage(url.toString())) return true;
|
|
14260
|
+
return /^DELETE$/i.test(request.method) && /\/(session|sessions\/(current|me)|sign_?in|log_?in)\/?$/i.test(url.pathname);
|
|
14261
|
+
}
|
|
14262
|
+
function isHostedSignIn(url) {
|
|
14263
|
+
const host = url.hostname.toLowerCase();
|
|
14264
|
+
if (host === "identitytoolkit.googleapis.com") return /\/accounts:(signIn|signUp)/i.test(url.pathname);
|
|
14265
|
+
if (host === "securetoken.googleapis.com") return /\/token$/i.test(url.pathname);
|
|
14266
|
+
if (host.endsWith(".supabase.co")) return /^\/auth\/v1\/(token|signup|verify|otp|magiclink)(\/|$)/i.test(url.pathname);
|
|
14267
|
+
return /^cognito-idp\.[a-z0-9-]+\.amazonaws\.com$/.test(host);
|
|
14268
|
+
}
|
|
14269
|
+
function isSignInSubmission(request, target) {
|
|
14270
|
+
if (/^(GET|HEAD|OPTIONS)$/i.test(request.method)) return false;
|
|
14271
|
+
if (!["document", "xhr", "fetch"].includes(request.resourceType)) return false;
|
|
14272
|
+
let url;
|
|
14273
|
+
let page;
|
|
14274
|
+
try {
|
|
14275
|
+
url = new URL(request.url);
|
|
14276
|
+
page = new URL(request.pageUrl);
|
|
14277
|
+
} catch {
|
|
14278
|
+
return false;
|
|
14279
|
+
}
|
|
14280
|
+
if (isSignOutRequest(request, target)) return false;
|
|
14281
|
+
const signInPath = /(^|[/._-])(log-?in|sign-?in|signin|sign_in|auth|oauth2?|token|session|sessions|password|otp|verify|mfa|2fa|challenge|identifier)([/._?-]|$)/i;
|
|
14282
|
+
if (onAppSite(url.hostname, target) && signInPath.test(url.pathname)) return true;
|
|
14283
|
+
if (onAppSite(page.hostname, target) && isHostedSignIn(url)) return true;
|
|
14284
|
+
const firstParty = url.hostname === page.hostname || registrableDomain(url.hostname) === registrableDomain(page.hostname);
|
|
14285
|
+
return firstParty && isSignInStep(request.pageUrl, target);
|
|
14286
|
+
}
|
|
14287
|
+
var LOGIN_PATH_SEGMENT = /^(log-?in|log_in|sign-?in|sign_in|signin-oidc|wp-login|sso|saml2?|oauth2?|authorize|authenticate|auth|passwordless|magic-?link)$/i;
|
|
14288
|
+
function isLoginStep(pageUrl, target) {
|
|
14289
|
+
let url;
|
|
14290
|
+
try {
|
|
14291
|
+
url = new URL(pageUrl);
|
|
14292
|
+
} catch {
|
|
14293
|
+
return false;
|
|
14294
|
+
}
|
|
14295
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
14296
|
+
const host = url.hostname.toLowerCase();
|
|
14297
|
+
if (host !== target.hostname.toLowerCase() && SIGN_IN_HOST_LABEL.test(host.split(".")[0] ?? "")) return true;
|
|
14298
|
+
const query = url.search.slice(1);
|
|
14299
|
+
if (SIGN_IN_QUERY.test(query) && !SIGN_OUT_QUERY.test(query)) return true;
|
|
14300
|
+
if (SIGN_IN_PATH.test(url.pathname)) return true;
|
|
14301
|
+
return pathSegments(url).some((segment) => LOGIN_PATH_SEGMENT.test(segment));
|
|
14302
|
+
}
|
|
14303
|
+
function isOnAppPastSignIn(pageUrl, target) {
|
|
14304
|
+
let url;
|
|
14305
|
+
try {
|
|
14306
|
+
url = new URL(pageUrl);
|
|
14307
|
+
} catch {
|
|
14308
|
+
return false;
|
|
14309
|
+
}
|
|
14310
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
14311
|
+
return onAppSite(url.hostname, target) && !isSignInStep(pageUrl, target);
|
|
14312
|
+
}
|
|
14313
|
+
function cookieKey(cookie) {
|
|
14314
|
+
return `${bareHost(cookie.domain)}\0${cookie.path ?? "/"}\0${cookie.name}`;
|
|
14315
|
+
}
|
|
14316
|
+
function originOnApp(origin, target) {
|
|
14317
|
+
try {
|
|
14318
|
+
return onAppSite(new URL(origin).hostname, target);
|
|
14319
|
+
} catch {
|
|
14320
|
+
return false;
|
|
14321
|
+
}
|
|
14322
|
+
}
|
|
14323
|
+
function indexedDbEntries(databases) {
|
|
14324
|
+
const out = /* @__PURE__ */ new Map();
|
|
14325
|
+
for (const db of databases ?? []) {
|
|
14326
|
+
if (!db || typeof db !== "object") continue;
|
|
14327
|
+
const name = String(db.name ?? "");
|
|
14328
|
+
out.set(name, JSON.stringify(db));
|
|
14329
|
+
}
|
|
14330
|
+
return out;
|
|
14331
|
+
}
|
|
14332
|
+
function gainedAppCredential(baseline, current, target) {
|
|
14333
|
+
const before = new Map(baseline.cookies.map((cookie) => [cookieKey(cookie), cookie.value]));
|
|
14334
|
+
for (const cookie of current.cookies) {
|
|
14335
|
+
if (!cookie.value || !credentialCookie(cookie, target)) continue;
|
|
14336
|
+
if (before.get(cookieKey(cookie)) !== cookie.value) return true;
|
|
14337
|
+
}
|
|
14338
|
+
const storageBefore = /* @__PURE__ */ new Map();
|
|
14339
|
+
for (const origin of baseline.origins) {
|
|
14340
|
+
for (const [kind, entries] of [["l", origin.localStorage], ["s", origin.sessionStorage ?? []]]) {
|
|
14341
|
+
for (const entry of entries) storageBefore.set(`${origin.origin}\0${kind}\0${entry.name}`, entry.value);
|
|
14342
|
+
}
|
|
14343
|
+
for (const [name, value] of indexedDbEntries(origin.indexedDB)) storageBefore.set(`${origin.origin}\0i\0${name}`, value);
|
|
14344
|
+
}
|
|
14345
|
+
for (const origin of current.origins) {
|
|
14346
|
+
if (!originOnApp(origin.origin, target)) continue;
|
|
14347
|
+
for (const [kind, entries] of [["l", origin.localStorage], ["s", origin.sessionStorage ?? []]]) {
|
|
14348
|
+
for (const entry of entries) {
|
|
14349
|
+
if (!entry.value || !AUTH_NAME.test(entry.name) || FLOW_COOKIE.test(entry.name)) continue;
|
|
14350
|
+
if (storageBefore.get(`${origin.origin}\0${kind}\0${entry.name}`) !== entry.value) return true;
|
|
14351
|
+
}
|
|
14352
|
+
}
|
|
14353
|
+
for (const [name, value] of indexedDbEntries(origin.indexedDB)) {
|
|
14354
|
+
if (!AUTH_NAME.test(name) && name !== "firebaseLocalStorageDb") continue;
|
|
14355
|
+
if (storageBefore.get(`${origin.origin}\0i\0${name}`) !== value) return true;
|
|
14356
|
+
}
|
|
14357
|
+
}
|
|
14358
|
+
return false;
|
|
14359
|
+
}
|
|
14360
|
+
var SESSION_NAME = /sess|session|sid|token|jwt|auth|refresh|remember|credential|bearer|^\.aspnetcore\.(cookies|identity)|^_\w+_key$/i;
|
|
14361
|
+
function credentialCookie(cookie, target) {
|
|
14362
|
+
if (!onAppSite(cookie.domain, target) || NOT_A_SIGN_IN_COOKIE.test(cookie.name) || FLOW_COOKIE.test(cookie.name)) return false;
|
|
14363
|
+
return Boolean(cookie.httpOnly) || AUTH_NAME.test(cookie.name);
|
|
14364
|
+
}
|
|
14365
|
+
var session = (cookie) => SESSION_NAME.test(cookie.name) && !NOT_A_SIGN_IN_COOKIE.test(cookie.name) && !FLOW_COOKIE.test(cookie.name);
|
|
14366
|
+
var authStorage = (entry) => AUTH_NAME.test(entry.name) && !FLOW_COOKIE.test(entry.name);
|
|
14367
|
+
var authDatabase = (db) => {
|
|
14368
|
+
const name = String(db?.name ?? "");
|
|
14369
|
+
return AUTH_NAME.test(name) || name === "firebaseLocalStorageDb";
|
|
14370
|
+
};
|
|
14371
|
+
function withoutCredentials(state2, target) {
|
|
14372
|
+
return {
|
|
14373
|
+
cookies: state2.cookies.filter((cookie) => onAppSite(cookie.domain, target) && !session(cookie)),
|
|
14374
|
+
origins: state2.origins.filter((origin) => originOnApp(origin.origin, target)).map((origin) => ({
|
|
14375
|
+
origin: origin.origin,
|
|
14376
|
+
localStorage: origin.localStorage.filter((entry) => !authStorage(entry)),
|
|
14377
|
+
...origin.indexedDB ? { indexedDB: origin.indexedDB.filter((db) => !authDatabase(db)) } : {}
|
|
14378
|
+
}))
|
|
14379
|
+
};
|
|
14380
|
+
}
|
|
14381
|
+
function droppedCredentials(state2, target) {
|
|
14382
|
+
const out = /* @__PURE__ */ new Set();
|
|
14383
|
+
for (const cookie of state2.cookies) if (onAppSite(cookie.domain, target) && session(cookie)) out.add(`c\0${cookieKey(cookie)}\0${cookie.value}`);
|
|
14384
|
+
for (const origin of state2.origins) {
|
|
14385
|
+
if (!originOnApp(origin.origin, target)) continue;
|
|
14386
|
+
for (const entry of origin.localStorage) if (authStorage(entry)) out.add(`l\0${origin.origin}\0${entry.name}\0${entry.value}`);
|
|
14387
|
+
for (const db of origin.indexedDB ?? []) if (authDatabase(db)) out.add(`i\0${origin.origin}\0${JSON.stringify(db)}`);
|
|
14388
|
+
}
|
|
14389
|
+
return out;
|
|
14390
|
+
}
|
|
14391
|
+
function credentialsKeptAcrossSignIn(before, after, target) {
|
|
14392
|
+
const kept = droppedCredentials(after, target);
|
|
14393
|
+
return [...droppedCredentials(before, target)].every((item) => kept.has(item));
|
|
14394
|
+
}
|
|
14395
|
+
function looksSignedIn(observation, target) {
|
|
14396
|
+
const urls = observation.openPageUrls.filter((url) => /^https?:/i.test(url));
|
|
14397
|
+
if (urls.length === 0 || !observation.submitted) return false;
|
|
14398
|
+
if (urls.some((url) => isSignInStep(url, target))) return false;
|
|
14399
|
+
if (!urls.some((url) => isOnAppPastSignIn(url, target))) return false;
|
|
14400
|
+
return gainedAppCredential(observation.baseline, observation.current, target);
|
|
14401
|
+
}
|
|
14402
|
+
|
|
14403
|
+
// src/services/sign-in-capture.ts
|
|
13836
14404
|
async function captureSignInInteractively(url, ready, opts = {}) {
|
|
13837
14405
|
const target = new URL(url);
|
|
13838
14406
|
let browser;
|
|
@@ -13841,47 +14409,316 @@ async function captureSignInInteractively(url, ready, opts = {}) {
|
|
|
13841
14409
|
} catch {
|
|
13842
14410
|
browser = await chromium.launch({ headless: false, ignoreDefaultArgs: ["--enable-automation"], args: [...import_google_local_signin.SIGN_IN_WINDOW_ARGS], env: (0, import_browser_config3.browserLaunchEnv)() });
|
|
13843
14411
|
}
|
|
13844
|
-
return captureFromBrowser(browser, target, ready, opts.timeoutMs ?? SIGN_IN_TIMEOUT_MS);
|
|
14412
|
+
return captureFromBrowser(browser, target, ready, opts.timeoutMs ?? SIGN_IN_TIMEOUT_MS, opts);
|
|
13845
14413
|
}
|
|
13846
14414
|
var SIGN_IN_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
13847
|
-
|
|
14415
|
+
var SIGN_IN_SNAPSHOT_INTERVAL_MS = 2e3;
|
|
14416
|
+
var PROVE_RETRY_MS = 1e4;
|
|
14417
|
+
var PROVE_DEADLINE_MS = 25e3;
|
|
14418
|
+
var CLOSE_PROVE_DEADLINE_MS = 3e4;
|
|
14419
|
+
async function readState(context, tabs = () => true, indexedDB = true) {
|
|
14420
|
+
const state2 = await context.storageState({ indexedDB });
|
|
14421
|
+
for (const open of context.pages()) {
|
|
14422
|
+
let origin;
|
|
14423
|
+
try {
|
|
14424
|
+
if (!tabs(open.url())) continue;
|
|
14425
|
+
origin = new URL(open.url()).origin;
|
|
14426
|
+
} catch {
|
|
14427
|
+
continue;
|
|
14428
|
+
}
|
|
14429
|
+
const entries = await open.evaluate(() => {
|
|
14430
|
+
const out = [];
|
|
14431
|
+
for (let i = 0; i < window.sessionStorage.length; i += 1) {
|
|
14432
|
+
const name = window.sessionStorage.key(i);
|
|
14433
|
+
if (name !== null) out.push({ name, value: window.sessionStorage.getItem(name) ?? "" });
|
|
14434
|
+
}
|
|
14435
|
+
return out;
|
|
14436
|
+
}).catch(() => []);
|
|
14437
|
+
if (entries.length === 0) continue;
|
|
14438
|
+
const entry = state2.origins.find((candidate) => candidate.origin === origin);
|
|
14439
|
+
if (entry) entry.sessionStorage = entries;
|
|
14440
|
+
else state2.origins.push({ origin, localStorage: [], sessionStorage: entries });
|
|
14441
|
+
}
|
|
14442
|
+
return state2;
|
|
14443
|
+
}
|
|
14444
|
+
async function readEnv(context) {
|
|
14445
|
+
const page0 = context.pages()[0];
|
|
14446
|
+
return page0 ? await page0.evaluate(() => ({ ua: navigator.userAgent, locale: navigator.language, tz: Intl.DateTimeFormat().resolvedOptions().timeZone })) : { ua: "", locale: "", tz: "" };
|
|
14447
|
+
}
|
|
14448
|
+
function toCapture(state2, env) {
|
|
14449
|
+
return { storageState: JSON.stringify(state2), userAgent: env?.ua ?? "", locale: env?.locale ?? "", timezoneId: env?.tz ?? "" };
|
|
14450
|
+
}
|
|
14451
|
+
function watchSignIn(context, target, opts = {}) {
|
|
14452
|
+
const now = opts.now ?? Date.now;
|
|
14453
|
+
let baseline;
|
|
14454
|
+
let submitted = false;
|
|
14455
|
+
let announced = false;
|
|
14456
|
+
let last;
|
|
14457
|
+
let beforeSubmit;
|
|
14458
|
+
let lastDurable = false;
|
|
14459
|
+
const durableCredential = (state2) => Boolean(baseline) && gainedAppCredential(
|
|
14460
|
+
{ cookies: baseline.cookies, origins: baseline.origins.map(({ sessionStorage: _s, ...rest }) => rest) },
|
|
14461
|
+
{ cookies: state2.cookies, origins: state2.origins.map(({ sessionStorage: _s, ...rest }) => rest) },
|
|
14462
|
+
target
|
|
14463
|
+
);
|
|
14464
|
+
let env;
|
|
14465
|
+
const indexedDbByOrigin = /* @__PURE__ */ new Map();
|
|
14466
|
+
let inFlight;
|
|
14467
|
+
let queued;
|
|
14468
|
+
let stopped = false;
|
|
14469
|
+
const prove = opts.proveSignedIn ?? proveSignedInHeadless;
|
|
14470
|
+
const aborts = new AbortController();
|
|
14471
|
+
let proving;
|
|
14472
|
+
let proven;
|
|
14473
|
+
let tried;
|
|
14474
|
+
const openUrls = () => context.pages().filter((open) => !open.isClosed()).map((open) => open.url());
|
|
14475
|
+
const signedOut = () => {
|
|
14476
|
+
submitted = false;
|
|
14477
|
+
last = void 0;
|
|
14478
|
+
lastDurable = false;
|
|
14479
|
+
proven = void 0;
|
|
14480
|
+
};
|
|
14481
|
+
const snapshot = async (full) => {
|
|
14482
|
+
if (baseline && openUrls().some((url) => isSignInStep(url, target))) {
|
|
14483
|
+
return;
|
|
14484
|
+
}
|
|
14485
|
+
const state2 = await readState(context, (url) => isOnAppPastSignIn(url, target), full);
|
|
14486
|
+
if (full) {
|
|
14487
|
+
for (const origin of state2.origins) if (origin.indexedDB) indexedDbByOrigin.set(origin.origin, origin.indexedDB);
|
|
14488
|
+
} else {
|
|
14489
|
+
for (const [origin, databases] of indexedDbByOrigin) {
|
|
14490
|
+
const entry = state2.origins.find((candidate) => candidate.origin === origin);
|
|
14491
|
+
if (entry) entry.indexedDB = databases;
|
|
14492
|
+
else state2.origins.push({ origin, localStorage: [], indexedDB: databases });
|
|
14493
|
+
}
|
|
14494
|
+
}
|
|
14495
|
+
if (!env) env = await readEnv(context).catch(() => void 0);
|
|
14496
|
+
if (!baseline) {
|
|
14497
|
+
baseline = state2;
|
|
14498
|
+
beforeSubmit = state2;
|
|
14499
|
+
return;
|
|
14500
|
+
}
|
|
14501
|
+
const openPageUrls = openUrls();
|
|
14502
|
+
if (openPageUrls.some((url) => isSignOutPage(url))) signedOut();
|
|
14503
|
+
if (last && lastDurable && !durableCredential(state2)) signedOut();
|
|
14504
|
+
if (!submitted) beforeSubmit = state2;
|
|
14505
|
+
if (looksSignedIn({ openPageUrls, submitted, baseline, current: state2 }, target)) {
|
|
14506
|
+
const candidate = {
|
|
14507
|
+
capture: toCapture(state2, env),
|
|
14508
|
+
pageUrl: openPageUrls.find((url) => isOnAppPastSignIn(url, target)) ?? target.toString(),
|
|
14509
|
+
...beforeSubmit ? { signedOut: JSON.stringify(beforeSubmit) } : {}
|
|
14510
|
+
};
|
|
14511
|
+
last = candidate;
|
|
14512
|
+
lastDurable = durableCredential(state2);
|
|
14513
|
+
const key = `${candidate.pageUrl}\0${candidate.capture.storageState}`;
|
|
14514
|
+
if (!announced && !proving && !stopped && !aborts.signal.aborted && tried?.key !== key && (!tried || now() - tried.at >= PROVE_RETRY_MS)) {
|
|
14515
|
+
tried = { key, at: now() };
|
|
14516
|
+
proving = prove(candidate.capture, candidate.pageUrl, target, { signal: aborts.signal, signedOut: candidate.signedOut }).catch(() => false).then((ok) => {
|
|
14517
|
+
proving = void 0;
|
|
14518
|
+
if (!ok || stopped || last === void 0) return;
|
|
14519
|
+
proven = candidate;
|
|
14520
|
+
if (!announced) {
|
|
14521
|
+
announced = true;
|
|
14522
|
+
opts.onLooksSignedIn?.();
|
|
14523
|
+
}
|
|
14524
|
+
});
|
|
14525
|
+
}
|
|
14526
|
+
}
|
|
14527
|
+
};
|
|
14528
|
+
const take = (full = true) => {
|
|
14529
|
+
if (stopped) return;
|
|
14530
|
+
if (inFlight) {
|
|
14531
|
+
queued = queued === "full" || full ? "full" : "light";
|
|
14532
|
+
return;
|
|
14533
|
+
}
|
|
14534
|
+
inFlight = snapshot(full).catch(() => {
|
|
14535
|
+
}).finally(() => {
|
|
14536
|
+
inFlight = void 0;
|
|
14537
|
+
const next = queued;
|
|
14538
|
+
queued = void 0;
|
|
14539
|
+
if (next && !stopped) take(next === "full");
|
|
14540
|
+
});
|
|
14541
|
+
};
|
|
14542
|
+
const watch = (page) => {
|
|
14543
|
+
page.on("framenavigated", (frame) => {
|
|
14544
|
+
if (frame !== page.mainFrame()) return;
|
|
14545
|
+
if (isSignOutPage(page.url())) signedOut();
|
|
14546
|
+
take();
|
|
14547
|
+
});
|
|
14548
|
+
page.on("load", () => take());
|
|
14549
|
+
};
|
|
14550
|
+
context.pages().forEach(watch);
|
|
14551
|
+
context.on("page", watch);
|
|
14552
|
+
context.on("request", (request) => {
|
|
14553
|
+
try {
|
|
14554
|
+
const frame = request.frame();
|
|
14555
|
+
if (frame.parentFrame()) return;
|
|
14556
|
+
const sent = { method: request.method(), resourceType: request.resourceType(), url: request.url(), pageUrl: frame.url() };
|
|
14557
|
+
if (isSignOutRequest(sent, target)) signedOut();
|
|
14558
|
+
else if (baseline && isSignInSubmission(sent, target)) submitted = true;
|
|
14559
|
+
} catch {
|
|
14560
|
+
}
|
|
14561
|
+
});
|
|
14562
|
+
const timer = setInterval(() => take(false), opts.snapshotIntervalMs ?? SIGN_IN_SNAPSHOT_INTERVAL_MS);
|
|
14563
|
+
timer.unref?.();
|
|
14564
|
+
take();
|
|
14565
|
+
return {
|
|
14566
|
+
take,
|
|
14567
|
+
// Wait (briefly) for the read in flight and the one queued behind it, then for a proof in flight
|
|
14568
|
+
// (the prover has its own time limits; stop() aborts it).
|
|
14569
|
+
settle: async () => {
|
|
14570
|
+
const deadline = now() + 5e3;
|
|
14571
|
+
while (inFlight && now() < deadline) {
|
|
14572
|
+
await Promise.race([inFlight, new Promise((resolve3) => setTimeout(resolve3, 250))]);
|
|
14573
|
+
}
|
|
14574
|
+
if (proving) await Promise.race([proving, new Promise((resolve3) => aborts.signal.aborted ? resolve3() : aborts.signal.addEventListener("abort", () => resolve3(), { once: true }))]);
|
|
14575
|
+
},
|
|
14576
|
+
stop: () => {
|
|
14577
|
+
stopped = true;
|
|
14578
|
+
clearInterval(timer);
|
|
14579
|
+
aborts.abort();
|
|
14580
|
+
},
|
|
14581
|
+
// Ends a background proof (and starts none): the close path proves the last state itself.
|
|
14582
|
+
abortProof: () => aborts.abort(),
|
|
14583
|
+
lastSignedIn: () => last,
|
|
14584
|
+
proven: () => proven
|
|
14585
|
+
};
|
|
14586
|
+
}
|
|
14587
|
+
async function captureFromBrowser(browser, target, ready, timeoutMs, opts = {}) {
|
|
13848
14588
|
let context;
|
|
14589
|
+
let watcher;
|
|
13849
14590
|
try {
|
|
13850
14591
|
context = await browser.newContext({ viewport: null });
|
|
13851
14592
|
const page = await context.newPage();
|
|
13852
14593
|
await page.goto(target.toString(), { waitUntil: "domcontentloaded" });
|
|
13853
|
-
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
|
|
14594
|
+
watcher = watchSignIn(context, target, opts);
|
|
14595
|
+
try {
|
|
14596
|
+
await readyUnlessClosed(ready, whenWindowClosed(browser, context), timeoutMs);
|
|
14597
|
+
} catch (error) {
|
|
14598
|
+
const name = error instanceof Error ? error.name : "";
|
|
14599
|
+
if (name !== "TargetClosedError" && name !== "SignInTimeoutError") throw error;
|
|
14600
|
+
watcher.abortProof();
|
|
14601
|
+
watcher.take();
|
|
14602
|
+
await watcher.settle();
|
|
14603
|
+
watcher.stop();
|
|
14604
|
+
const saved = watcher.lastSignedIn();
|
|
14605
|
+
const earlier = watcher.proven();
|
|
14606
|
+
if (!saved) throw error;
|
|
14607
|
+
const prove = opts.proveSignedIn ?? proveSignedInHeadless;
|
|
14608
|
+
const pages = [.../* @__PURE__ */ new Set([saved.pageUrl, ...earlier ? [earlier.pageUrl] : []])];
|
|
14609
|
+
const bound = new AbortController();
|
|
14610
|
+
const timer = setTimeout(() => bound.abort(), opts.closeProveDeadlineMs ?? CLOSE_PROVE_DEADLINE_MS);
|
|
14611
|
+
timer.unref?.();
|
|
13857
14612
|
try {
|
|
13858
|
-
|
|
13859
|
-
|
|
13860
|
-
|
|
13861
|
-
|
|
13862
|
-
|
|
13863
|
-
|
|
13864
|
-
for (let i = 0; i < window.sessionStorage.length; i += 1) {
|
|
13865
|
-
const name = window.sessionStorage.key(i);
|
|
13866
|
-
if (name !== null) out.push({ name, value: window.sessionStorage.getItem(name) ?? "" });
|
|
14613
|
+
for (const pageUrl of pages) {
|
|
14614
|
+
if (bound.signal.aborted) break;
|
|
14615
|
+
const ended = new Promise((resolve3) => bound.signal.addEventListener("abort", () => resolve3(false), { once: true }));
|
|
14616
|
+
if (await Promise.race([prove(saved.capture, pageUrl, target, { signal: bound.signal, signedOut: saved.signedOut }).catch(() => false), ended])) {
|
|
14617
|
+
return { ...saved.capture, savedWithoutEnter: true };
|
|
14618
|
+
}
|
|
13867
14619
|
}
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
|
|
13871
|
-
|
|
13872
|
-
|
|
13873
|
-
|
|
13874
|
-
|
|
13875
|
-
|
|
13876
|
-
|
|
13877
|
-
|
|
14620
|
+
} finally {
|
|
14621
|
+
clearTimeout(timer);
|
|
14622
|
+
}
|
|
14623
|
+
throw error;
|
|
14624
|
+
}
|
|
14625
|
+
watcher.stop();
|
|
14626
|
+
await watcher.settle();
|
|
14627
|
+
try {
|
|
14628
|
+
return toCapture(await readState(context), await readEnv(context));
|
|
14629
|
+
} catch (error) {
|
|
14630
|
+
const saved = watcher.lastSignedIn();
|
|
14631
|
+
if (saved) return saved.capture;
|
|
14632
|
+
throw error;
|
|
14633
|
+
}
|
|
13878
14634
|
} finally {
|
|
14635
|
+
watcher?.stop();
|
|
13879
14636
|
await context?.close().catch(() => {
|
|
13880
14637
|
});
|
|
13881
14638
|
await browser.close().catch(() => {
|
|
13882
14639
|
});
|
|
13883
14640
|
}
|
|
13884
14641
|
}
|
|
14642
|
+
function provenByProbe(signedIn, signedOut, target, anonymous) {
|
|
14643
|
+
if (isSignInStep(signedIn.url, target) || signedIn.offersSignIn) return false;
|
|
14644
|
+
const signedOutThere = (side) => isLoginStep(side.url, target) || side.offersSignIn;
|
|
14645
|
+
return signedOutThere(signedOut) && (anonymous === void 0 || signedOutThere(anonymous));
|
|
14646
|
+
}
|
|
14647
|
+
var OFFERS_SIGN_IN = `(() => {
|
|
14648
|
+
const shown = (el) => { const box = el.getBoundingClientRect(); return box.width > 0 && box.height > 0; };
|
|
14649
|
+
if ([...document.querySelectorAll('input[type="password"]')].some(shown)) return true;
|
|
14650
|
+
return [...document.querySelectorAll('a, button, [role="button"]')].some((el) => shown(el) && /^\\s*(log ?in|sign ?in|sign in with .+|continue with google)\\s*$/i.test(el.innerText || ''));
|
|
14651
|
+
})()`;
|
|
14652
|
+
var chromeChannelMissing = false;
|
|
14653
|
+
async function launchProver() {
|
|
14654
|
+
if (!chromeChannelMissing) {
|
|
14655
|
+
try {
|
|
14656
|
+
return await chromium.launch({ headless: true, channel: "chrome", env: (0, import_browser_config3.browserLaunchEnv)() });
|
|
14657
|
+
} catch {
|
|
14658
|
+
chromeChannelMissing = true;
|
|
14659
|
+
}
|
|
14660
|
+
}
|
|
14661
|
+
return chromium.launch({ headless: true, env: (0, import_browser_config3.browserLaunchEnv)() });
|
|
14662
|
+
}
|
|
14663
|
+
async function proveSignedInHeadless(capture, pageUrl, target, options = {}) {
|
|
14664
|
+
const ended = new AbortController();
|
|
14665
|
+
const end = () => ended.abort();
|
|
14666
|
+
const timer = setTimeout(end, options.deadlineMs ?? PROVE_DEADLINE_MS);
|
|
14667
|
+
timer.unref?.();
|
|
14668
|
+
options.signal?.addEventListener("abort", end);
|
|
14669
|
+
if (options.signal?.aborted) end();
|
|
14670
|
+
const gone = new Promise((_resolve, reject) => {
|
|
14671
|
+
if (ended.signal.aborted) reject(new Error("proof ended"));
|
|
14672
|
+
else ended.signal.addEventListener("abort", () => reject(new Error("proof ended")), { once: true });
|
|
14673
|
+
});
|
|
14674
|
+
gone.catch(() => {
|
|
14675
|
+
});
|
|
14676
|
+
const live = (work) => Promise.race([work, gone]);
|
|
14677
|
+
let launching;
|
|
14678
|
+
try {
|
|
14679
|
+
if (ended.signal.aborted) return false;
|
|
14680
|
+
launching = (options.launch ?? launchProver)();
|
|
14681
|
+
const browser = await live(launching);
|
|
14682
|
+
const probe = async (storageState) => {
|
|
14683
|
+
const context = await live(browser.newContext({ storageState, ...capture.userAgent ? { userAgent: capture.userAgent } : {} }));
|
|
14684
|
+
try {
|
|
14685
|
+
const page = await live(context.newPage());
|
|
14686
|
+
await live(page.goto(pageUrl, { waitUntil: "domcontentloaded", timeout: 3e4 }));
|
|
14687
|
+
await live(page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
|
|
14688
|
+
}));
|
|
14689
|
+
const offersSignIn = await live(page.evaluate(OFFERS_SIGN_IN)) === true;
|
|
14690
|
+
return { url: page.url(), offersSignIn };
|
|
14691
|
+
} finally {
|
|
14692
|
+
void context.close().catch(() => {
|
|
14693
|
+
});
|
|
14694
|
+
}
|
|
14695
|
+
};
|
|
14696
|
+
const forContext = (state2) => ({ cookies: state2.cookies, origins: state2.origins.map(({ origin, localStorage, indexedDB }) => ({ origin, localStorage, ...indexedDB ? { indexedDB } : {} })) });
|
|
14697
|
+
const after = JSON.parse(capture.storageState);
|
|
14698
|
+
const signedIn = await probe(forContext(after));
|
|
14699
|
+
if (isSignInStep(signedIn.url, target) || signedIn.offersSignIn) return false;
|
|
14700
|
+
const before = options.signedOut ? JSON.parse(options.signedOut) : { cookies: [], origins: [] };
|
|
14701
|
+
const asIs = options.signedOut && !credentialsKeptAcrossSignIn(before, after, target) ? before : void 0;
|
|
14702
|
+
const [signedOut, anonymous] = await Promise.all([
|
|
14703
|
+
probe(forContext(withoutCredentials(before, target))),
|
|
14704
|
+
asIs ? probe(forContext(asIs)) : Promise.resolve(void 0)
|
|
14705
|
+
]);
|
|
14706
|
+
return provenByProbe(signedIn, signedOut, target, anonymous);
|
|
14707
|
+
} catch {
|
|
14708
|
+
return false;
|
|
14709
|
+
} finally {
|
|
14710
|
+
clearTimeout(timer);
|
|
14711
|
+
options.signal?.removeEventListener("abort", end);
|
|
14712
|
+
end();
|
|
14713
|
+
if (launching) {
|
|
14714
|
+
const browser = await Promise.race([launching.catch(() => void 0), new Promise((resolve3) => setTimeout(resolve3, 1e3))]);
|
|
14715
|
+
if (browser) await Promise.race([browser.close().catch(() => {
|
|
14716
|
+
}), new Promise((resolve3) => setTimeout(resolve3, 5e3))]);
|
|
14717
|
+
else void launching.then((late) => late.close()).catch(() => {
|
|
14718
|
+
});
|
|
14719
|
+
}
|
|
14720
|
+
}
|
|
14721
|
+
}
|
|
13885
14722
|
function whenWindowClosed(browser, context) {
|
|
13886
14723
|
return new Promise((resolve3) => {
|
|
13887
14724
|
const watch = (page) => {
|
|
@@ -13940,7 +14777,22 @@ function closedTargetError() {
|
|
|
13940
14777
|
error.name = "TargetClosedError";
|
|
13941
14778
|
return error;
|
|
13942
14779
|
}
|
|
14780
|
+
var LIVE_SIGN_IN_KEPT_NOTICE = "A sign-in is already saved for this project, and this one was not confirmed with Enter (the window was closed or timed out), so the saved one was kept and nothing was replaced. To replace it, run sign-in again and press Enter once you are signed in.";
|
|
13943
14781
|
async function uploadSignInCapture(input) {
|
|
14782
|
+
if (!input.capture.savedWithoutEnter) {
|
|
14783
|
+
const sent = await postCapture(input, true);
|
|
14784
|
+
return sent === "live_sign_in_exists" ? { ok: false, message: LIVE_SIGN_IN_KEPT_NOTICE } : sent;
|
|
14785
|
+
}
|
|
14786
|
+
const first = await postCapture(input, false);
|
|
14787
|
+
if (first !== "live_sign_in_exists") return first;
|
|
14788
|
+
if (!input.confirmReplace) return { ok: false, message: LIVE_SIGN_IN_KEPT_NOTICE };
|
|
14789
|
+
if (!await input.confirmReplace().catch(() => false)) {
|
|
14790
|
+
return { ok: true, kept: true, message: "Kept the sign-in already saved. The one from this window was not uploaded." };
|
|
14791
|
+
}
|
|
14792
|
+
const replaced = await postCapture(input, true);
|
|
14793
|
+
return replaced === "live_sign_in_exists" ? { ok: false, message: LIVE_SIGN_IN_KEPT_NOTICE } : replaced;
|
|
14794
|
+
}
|
|
14795
|
+
async function postCapture(input, replace) {
|
|
13944
14796
|
const doFetch = input.fetchImpl ?? fetch;
|
|
13945
14797
|
try {
|
|
13946
14798
|
const res = await doFetch(`${input.serverUrl.replace(/\/+$/, "")}/api/runner/saved-sign-in/capture`, {
|
|
@@ -13953,14 +14805,21 @@ async function uploadSignInCapture(input) {
|
|
|
13953
14805
|
storageState: input.capture.storageState,
|
|
13954
14806
|
userAgent: input.capture.userAgent,
|
|
13955
14807
|
locale: input.capture.locale,
|
|
13956
|
-
timezoneId: input.capture.timezoneId
|
|
14808
|
+
timezoneId: input.capture.timezoneId,
|
|
14809
|
+
// Only for a save without Enter: a save after Enter is sent exactly as before.
|
|
14810
|
+
...replace ? {} : { replaceExisting: false }
|
|
13957
14811
|
}),
|
|
13958
14812
|
signal: AbortSignal.timeout(6e4)
|
|
13959
14813
|
});
|
|
13960
14814
|
const body = await res.json().catch(() => ({}));
|
|
14815
|
+
if (!replace && res.status === 409 && body.reason === "live_sign_in_exists") return "live_sign_in_exists";
|
|
13961
14816
|
if (res.ok) return { ok: true, message: "Sign-in saved. Surveys and tests for this project will run signed in." };
|
|
13962
14817
|
if (res.status === 403) return { ok: false, message: body.error ?? "Only project admins can save a sign-in." };
|
|
13963
|
-
if (res.status === 422)
|
|
14818
|
+
if (res.status === 422) {
|
|
14819
|
+
if (typeof body.error === "string" && body.error.trim()) return { ok: false, message: `The sign-in was not saved: ${body.error.trim()}.` };
|
|
14820
|
+
return { ok: false, message: "The browser was not signed in to your app (nothing of the app was found). Sign in, then try again." };
|
|
14821
|
+
}
|
|
14822
|
+
if (res.status === 413) return { ok: false, message: "The sign-in is too large to save (the server takes up to 4 MB). Sign in with fewer tabs open, or clear unrelated site data, then try again." };
|
|
13964
14823
|
return { ok: false, message: `Could not save the sign-in (HTTP ${res.status}${body.error ? `: ${body.error}` : ""}).` };
|
|
13965
14824
|
} catch {
|
|
13966
14825
|
return { ok: false, message: "Could not reach the validate.qa server to save the sign-in." };
|
|
@@ -14026,6 +14885,43 @@ function shownAddress(url) {
|
|
|
14026
14885
|
return oneLine.replace(/^([a-z][a-z0-9+.-]*:\/\/)?\S*@/i, "$1");
|
|
14027
14886
|
}
|
|
14028
14887
|
|
|
14888
|
+
// src/sign-in-url.ts
|
|
14889
|
+
var OPENABLE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
14890
|
+
var SIGN_IN_URL_HINT = "--url must be a full address starting with http:// or https://, e.g. https://app.example.com/login or http://localhost:3000.";
|
|
14891
|
+
function signInUrlProblem(raw) {
|
|
14892
|
+
try {
|
|
14893
|
+
const url = new URL(raw);
|
|
14894
|
+
if (OPENABLE_PROTOCOLS.has(url.protocol) && url.hostname) return null;
|
|
14895
|
+
} catch {
|
|
14896
|
+
}
|
|
14897
|
+
return SIGN_IN_URL_HINT;
|
|
14898
|
+
}
|
|
14899
|
+
|
|
14900
|
+
// src/no-credentials-help.ts
|
|
14901
|
+
var RUNNER_TOKEN_SOURCE = "Project Settings \u2192 Local Runner \u2192 Generate Token (urt_...)";
|
|
14902
|
+
function noSignInCredentialsMessage() {
|
|
14903
|
+
return [
|
|
14904
|
+
"\u2717 No runner credentials on this machine yet.",
|
|
14905
|
+
` Get a runner token (urt_...) in validate.qa: ${RUNNER_TOKEN_SOURCE}.`,
|
|
14906
|
+
" Then either:",
|
|
14907
|
+
" \u2022 sign in in one go: npx @validate.qa/runner sign-in --url <your-app> --token <urt_...> --server <url> --project <id>",
|
|
14908
|
+
" \u2022 or log in once (paste the token at its prompt, which keeps it out of shell history), then sign in:",
|
|
14909
|
+
" npx @validate.qa/runner login --server <url>"
|
|
14910
|
+
].join("\n");
|
|
14911
|
+
}
|
|
14912
|
+
function noStartTokenMessage() {
|
|
14913
|
+
return [
|
|
14914
|
+
"\u2717 No runner token.",
|
|
14915
|
+
"",
|
|
14916
|
+
"Steps:",
|
|
14917
|
+
` 1. In validate.qa: ${RUNNER_TOKEN_SOURCE}`,
|
|
14918
|
+
" 2. npx @validate.qa/runner login --server <url> (the server URL printed above or from the dashboard;",
|
|
14919
|
+
" paste the token at its prompt, which keeps it out of shell history)",
|
|
14920
|
+
"",
|
|
14921
|
+
" Or pass --token and --server flags to this start command."
|
|
14922
|
+
].join("\n");
|
|
14923
|
+
}
|
|
14924
|
+
|
|
14029
14925
|
// src/supervisor-child-argv.ts
|
|
14030
14926
|
function stripRunnerTokenFromArgv(argv) {
|
|
14031
14927
|
const out = [];
|
|
@@ -14064,23 +14960,48 @@ program.name("validate-runner").description("validate.qa local test runner - exe
|
|
|
14064
14960
|
async function requireLatestRunner() {
|
|
14065
14961
|
await enforceRunnerUpToDate(pkg.version);
|
|
14066
14962
|
}
|
|
14067
|
-
program.command("login").description("Save runner credentials for future use").
|
|
14963
|
+
program.command("login").description("Save runner credentials for future use").option("--token <token>", "Runner token (urt_... from validate.qa dashboard). Leave it out to paste it at a prompt that does not show it, or pipe it on stdin: on the command line it lands in shell history").requiredOption("--server <url>", "validate.qa server URL").option("--project <id>", "Project ID (from dashboard URL)").action(async (opts) => {
|
|
14068
14964
|
await requireLatestRunner();
|
|
14069
14965
|
const serverUrl = opts.server.replace(/\/+$/, "");
|
|
14070
|
-
const
|
|
14966
|
+
const urlProblem = loginServerUrlProblem(serverUrl);
|
|
14967
|
+
if (urlProblem) {
|
|
14968
|
+
console.error(`\u2717 ${urlProblem}`);
|
|
14969
|
+
process.exit(1);
|
|
14970
|
+
}
|
|
14971
|
+
let resolved;
|
|
14972
|
+
try {
|
|
14973
|
+
resolved = await resolveLoginToken({
|
|
14974
|
+
flag: opts.token,
|
|
14975
|
+
stdinIsTTY: process.stdin.isTTY === true,
|
|
14976
|
+
readPiped: () => readPipedToken(process.stdin),
|
|
14977
|
+
promptHidden: (question) => promptHidden(process.stdin, process.stdout, question)
|
|
14978
|
+
});
|
|
14979
|
+
} catch (err) {
|
|
14980
|
+
console.error(`\u2717 No runner token: ${err instanceof Error ? err.message : String(err)}`);
|
|
14981
|
+
process.exit(1);
|
|
14982
|
+
}
|
|
14983
|
+
if ("error" in resolved) {
|
|
14984
|
+
console.error(`\u2717 ${resolved.error}`);
|
|
14985
|
+
process.exit(1);
|
|
14986
|
+
}
|
|
14987
|
+
const token = resolved.token;
|
|
14988
|
+
const shapeProblem = runnerTokenShapeProblem(token);
|
|
14071
14989
|
if (shapeProblem) {
|
|
14072
14990
|
console.error(`\u2717 ${shapeProblem}`);
|
|
14073
14991
|
process.exit(1);
|
|
14074
14992
|
}
|
|
14993
|
+
if (sendsTokenInPlaintext(serverUrl)) {
|
|
14994
|
+
console.warn("\u26A0 WARNING: Server URL uses HTTP, not HTTPS. Runner token will be sent in plaintext.");
|
|
14995
|
+
}
|
|
14075
14996
|
console.log(`Testing connection to ${serverUrl}...`);
|
|
14076
|
-
const check2 = await checkRunnerLogin(serverUrl,
|
|
14997
|
+
const check2 = await checkRunnerLogin(serverUrl, token);
|
|
14077
14998
|
if (!check2.ok) {
|
|
14078
14999
|
console.error(`\u2717 ${check2.message}`);
|
|
14079
15000
|
process.exit(1);
|
|
14080
15001
|
}
|
|
14081
15002
|
console.log("\u2713 Connected successfully");
|
|
14082
15003
|
saveCredentials({
|
|
14083
|
-
token
|
|
15004
|
+
token,
|
|
14084
15005
|
serverUrl,
|
|
14085
15006
|
projectId: opts.project
|
|
14086
15007
|
});
|
|
@@ -14165,17 +15086,10 @@ program.command("start").description("Start the local test runner").option("--to
|
|
|
14165
15086
|
}
|
|
14166
15087
|
await requireLatestRunner();
|
|
14167
15088
|
const saved = loadCredentials();
|
|
14168
|
-
const authToken = opts
|
|
14169
|
-
const serverUrl = (opts.server || process.env.SERVER_URL || saved?.serverUrl)?.replace(/\/+$/, "");
|
|
14170
|
-
const projectId = opts.project || process.env.PROJECT_ID || saved?.projectId;
|
|
15089
|
+
const { authToken, serverUrl, projectId, savedLoginSkipped } = resolveCredentials(opts, process.env, saved);
|
|
14171
15090
|
if (!authToken) {
|
|
14172
|
-
console.error(
|
|
14173
|
-
console.error(
|
|
14174
|
-
console.error("Steps:");
|
|
14175
|
-
console.error(" 1. In validate.qa UI: Project Settings \u2192 Local Runner \u2192 Generate Token (urt_...)");
|
|
14176
|
-
console.error(" 2. npx @validate.qa/runner login --token <urt_...> --server <the-server-url-printed-above-or-from-dashboard>");
|
|
14177
|
-
console.error("");
|
|
14178
|
-
console.error(" Or pass --token and --server flags to this start command.");
|
|
15091
|
+
if (savedLoginSkipped) console.error(`\u2717 The saved login is for ${saved?.serverUrl}, not ${serverUrl}: its token is not sent to another server.`);
|
|
15092
|
+
console.error(noStartTokenMessage());
|
|
14179
15093
|
process.exit(EXIT_TERMINAL);
|
|
14180
15094
|
}
|
|
14181
15095
|
if (!serverUrl) {
|
|
@@ -14183,12 +15097,19 @@ program.command("start").description("Start the local test runner").option("--to
|
|
|
14183
15097
|
console.error(" Or pass --server <url> to this command");
|
|
14184
15098
|
process.exit(EXIT_TERMINAL);
|
|
14185
15099
|
}
|
|
15100
|
+
{
|
|
15101
|
+
const urlProblem = loginServerUrlProblem(serverUrl, opts.server ? "--server" : "The server URL");
|
|
15102
|
+
if (urlProblem) {
|
|
15103
|
+
console.error(`\u2717 ${urlProblem}`);
|
|
15104
|
+
process.exit(EXIT_TERMINAL);
|
|
15105
|
+
}
|
|
15106
|
+
}
|
|
14186
15107
|
const isPersonalToken = authToken.startsWith("urt_");
|
|
14187
15108
|
if (!projectId && !isPersonalToken) {
|
|
14188
15109
|
console.error("\u2717 No project ID. Pass --project <id> or set PROJECT_ID env var");
|
|
14189
15110
|
process.exit(EXIT_TERMINAL);
|
|
14190
15111
|
}
|
|
14191
|
-
if (
|
|
15112
|
+
if (sendsTokenInPlaintext(serverUrl)) {
|
|
14192
15113
|
console.warn("\u26A0 WARNING: Server URL uses HTTP, not HTTPS. Runner token will be sent in plaintext.");
|
|
14193
15114
|
console.warn(" Consider using an HTTPS URL to protect credentials in transit.");
|
|
14194
15115
|
}
|
|
@@ -14207,7 +15128,14 @@ program.command("start").description("Start the local test runner").option("--to
|
|
|
14207
15128
|
console.warn("\u26A0 --tunnel has no effect without --mobile (the mobile bridge it exposes only starts with --mobile). Ignoring --tunnel.");
|
|
14208
15129
|
}
|
|
14209
15130
|
{
|
|
14210
|
-
const
|
|
15131
|
+
const serverProblem = startServerProblem(await checkRunnerLogin(serverUrl, authToken), serverUrl);
|
|
15132
|
+
if (serverProblem) {
|
|
15133
|
+
console.error(`\u2717 ${serverProblem}`);
|
|
15134
|
+
process.exit(EXIT_TERMINAL);
|
|
15135
|
+
}
|
|
15136
|
+
}
|
|
15137
|
+
{
|
|
15138
|
+
const { hasLocalGoogleLogin, GOOGLE_LOGIN_COMMAND } = await import("./dist-BXHLIG5H.mjs");
|
|
14211
15139
|
if (process.stdout.isTTY && !await hasLocalGoogleLogin()) {
|
|
14212
15140
|
console.log(`Tip: for apps with "Continue with Google", sign in to Google once on this machine: ${GOOGLE_LOGIN_COMMAND}`);
|
|
14213
15141
|
}
|
|
@@ -14237,7 +15165,7 @@ async function askLine(question) {
|
|
|
14237
15165
|
}
|
|
14238
15166
|
}
|
|
14239
15167
|
program.command("google-login").description('Sign in to a TEST Google account once on this machine, for tests of apps with "Continue with Google"').option("--site <url...>", "The app address(es) this sign-in may be used for, e.g. https://app.example.com").option("--allow-file-storage", "Keep the sign-in in a plain file (only you can read it) when the system keychain is not available").option("--yes", "Confirm without asking that this is a dedicated test Google account").action(async (opts) => {
|
|
14240
|
-
const { signInLocalGoogle, LOCAL_GOOGLE_WARNING, grantedSite, defaultStoreKind } = await import("./dist-
|
|
15168
|
+
const { signInLocalGoogle, LOCAL_GOOGLE_WARNING, grantedSite, defaultStoreKind } = await import("./dist-BXHLIG5H.mjs");
|
|
14241
15169
|
console.log(LOCAL_GOOGLE_WARNING);
|
|
14242
15170
|
if (!opts.yes) {
|
|
14243
15171
|
const answer = await askLine('Is this a dedicated test Google account? Type "yes" to continue: ');
|
|
@@ -14272,7 +15200,7 @@ program.command("google-login").description('Sign in to a TEST Google account on
|
|
|
14272
15200
|
}
|
|
14273
15201
|
});
|
|
14274
15202
|
program.command("google-allow-site <url>").description("Let the Google sign-in on this machine be used for one more app address").action(async (url) => {
|
|
14275
|
-
const { allowLocalGoogleSites, grantedSite } = await import("./dist-
|
|
15203
|
+
const { allowLocalGoogleSites, grantedSite } = await import("./dist-BXHLIG5H.mjs");
|
|
14276
15204
|
const site = grantedSite(url);
|
|
14277
15205
|
if (!site) {
|
|
14278
15206
|
console.error("\u2717 Not a web address: give it like https://app.example.com");
|
|
@@ -14286,17 +15214,26 @@ program.command("google-allow-site <url>").description("Let the Google sign-in o
|
|
|
14286
15214
|
console.log(`\u2713 Tests on this machine may now use the Google sign-in for: ${sites.join(", ")}`);
|
|
14287
15215
|
});
|
|
14288
15216
|
program.command("google-logout").description("Forget the Google sign-in saved on this machine").action(async () => {
|
|
14289
|
-
const { deleteLocalGoogleLogin } = await import("./dist-
|
|
15217
|
+
const { deleteLocalGoogleLogin } = await import("./dist-BXHLIG5H.mjs");
|
|
14290
15218
|
await deleteLocalGoogleLogin();
|
|
14291
15219
|
console.log("\u2713 Google sign-in removed from this machine");
|
|
14292
15220
|
});
|
|
14293
15221
|
program.command("sign-in").description("Sign in to your app once in a browser window; surveys and tests then run signed in").requiredOption("--url <url>", "Your app's sign-in page or home page").option("--token <token>", "Runner token (overrides saved credentials)").option("--server <url>", "validate.qa server URL (overrides saved credentials)").option("--project <id>", "Project ID (overrides saved credentials)").action(async (opts) => {
|
|
14294
15222
|
const saved = loadCredentials();
|
|
14295
|
-
const token = opts
|
|
14296
|
-
const serverUrl = (opts.server || process.env.SERVER_URL || saved?.serverUrl)?.replace(/\/+$/, "");
|
|
14297
|
-
const projectId = opts.project || process.env.PROJECT_ID || saved?.projectId;
|
|
15223
|
+
const { authToken: token, serverUrl, projectId, savedLoginSkipped } = resolveCredentials(opts, process.env, saved);
|
|
14298
15224
|
if (!token || !serverUrl) {
|
|
14299
|
-
console.error(
|
|
15225
|
+
if (savedLoginSkipped) console.error(`\u2717 The saved login is for ${saved?.serverUrl}, not ${serverUrl}: pass --token for that server.`);
|
|
15226
|
+
console.error(noSignInCredentialsMessage());
|
|
15227
|
+
process.exit(1);
|
|
15228
|
+
}
|
|
15229
|
+
const serverUrlProblem = loginServerUrlProblem(serverUrl, opts.server ? "--server" : "The server URL");
|
|
15230
|
+
if (serverUrlProblem) {
|
|
15231
|
+
console.error(`\u2717 ${serverUrlProblem}`);
|
|
15232
|
+
process.exit(1);
|
|
15233
|
+
}
|
|
15234
|
+
const urlProblem = signInUrlProblem(opts.url);
|
|
15235
|
+
if (urlProblem) {
|
|
15236
|
+
console.error(`\u2717 ${urlProblem}`);
|
|
14300
15237
|
process.exit(1);
|
|
14301
15238
|
}
|
|
14302
15239
|
let appUrl;
|
|
@@ -14304,7 +15241,7 @@ program.command("sign-in").description("Sign in to your app once in a browser wi
|
|
|
14304
15241
|
try {
|
|
14305
15242
|
appUrl = new URL(opts.url);
|
|
14306
15243
|
console.log(`Opening ${appUrl.origin} in a browser window.`);
|
|
14307
|
-
console.log("Sign in to your app there with a test account. Then
|
|
15244
|
+
console.log("Sign in to your app there with a test account. Then press Enter here, or just close the window.");
|
|
14308
15245
|
capture = await captureSignInInteractively(appUrl.toString(), async (signal) => {
|
|
14309
15246
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
14310
15247
|
try {
|
|
@@ -14315,6 +15252,9 @@ program.command("sign-in").description("Sign in to your app once in a browser wi
|
|
|
14315
15252
|
} finally {
|
|
14316
15253
|
rl.close();
|
|
14317
15254
|
}
|
|
15255
|
+
}, {
|
|
15256
|
+
// Once: the prompt's line is still open, so the notice gets its own line.
|
|
15257
|
+
onLooksSignedIn: () => process.stdout.write("\n\u2713 Looks like you're signed in. You can press Enter or just close the window.\n")
|
|
14318
15258
|
});
|
|
14319
15259
|
} catch (error) {
|
|
14320
15260
|
const message = describeSignInFailure(error, opts.url);
|
|
@@ -14327,43 +15267,23 @@ program.command("sign-in").description("Sign in to your app once in a browser wi
|
|
|
14327
15267
|
token,
|
|
14328
15268
|
projectId,
|
|
14329
15269
|
label: `Signed in on ${appUrl.hostname}`,
|
|
14330
|
-
capture
|
|
15270
|
+
capture,
|
|
15271
|
+
// Asked only for a save without Enter that meets a saved sign-in; no terminal keeps the saved one.
|
|
15272
|
+
...process.stdin.isTTY ? {
|
|
15273
|
+
confirmReplace: async () => /^y(es)?$/i.test(await askLine("A sign-in is already saved for this project. Replace it with the one from this window? [y/N] "))
|
|
15274
|
+
} : {}
|
|
14331
15275
|
});
|
|
14332
|
-
console.log(result.ok ? `\u2713 ${result.message}` : `\u2717 ${result.message}`);
|
|
15276
|
+
console.log(result.kept ? `\u2139 ${result.message}` : result.ok ? `\u2713 ${result.message}` : `\u2717 ${result.message}`);
|
|
14333
15277
|
process.exit(result.ok ? 0 : 1);
|
|
14334
15278
|
});
|
|
14335
|
-
program.command("status").description("Check connection to validate.qa server").action(async () => {
|
|
15279
|
+
program.command("status").description("Check connection to validate.qa server, with the credentials start would use").option("--token <token>", "Runner token (overrides AUTH_TOKEN and saved credentials)").option("--server <url>", "Server URL (overrides SERVER_URL and saved credentials)").option("--project <id>", "Project ID (overrides PROJECT_ID and saved credentials)").action(async (opts) => {
|
|
14336
15280
|
await requireLatestRunner();
|
|
14337
|
-
|
|
14338
|
-
if (!saved) {
|
|
14339
|
-
console.log("No saved credentials. Run: validate-runner login");
|
|
14340
|
-
return;
|
|
14341
|
-
}
|
|
14342
|
-
console.log(`Server: ${saved.serverUrl}`);
|
|
14343
|
-
console.log(`Project: ${saved.projectId || "(not set)"}`);
|
|
14344
|
-
console.log(`Token: ${saved.token.slice(0, 8)}...${saved.token.slice(-4)}`);
|
|
14345
|
-
console.log("");
|
|
14346
|
-
try {
|
|
14347
|
-
const res = await fetch(`${saved.serverUrl}/api/runner/health`, {
|
|
14348
|
-
headers: {
|
|
14349
|
-
Authorization: `Bearer ${saved.token}`,
|
|
14350
|
-
...saved.projectId ? { "X-Project-Id": saved.projectId } : {}
|
|
14351
|
-
}
|
|
14352
|
-
});
|
|
14353
|
-
if (res.ok) {
|
|
14354
|
-
const data = await res.json();
|
|
14355
|
-
console.log(`\u2713 Server reachable - runner status: ${data.status} (${data.runners} runner(s) online)`);
|
|
14356
|
-
} else {
|
|
14357
|
-
console.log(`\u26A0 Server returned ${res.status}`);
|
|
14358
|
-
}
|
|
14359
|
-
} catch {
|
|
14360
|
-
console.error("\u2717 Could not reach server");
|
|
14361
|
-
}
|
|
15281
|
+
process.exitCode = await runStatusCheck(resolveStatusCredentials(opts, process.env, loadCredentials()));
|
|
14362
15282
|
});
|
|
14363
|
-
program.command("doctor").description("Check prerequisites for web and mobile testing").action(() => {
|
|
15283
|
+
program.command("doctor").description("Check prerequisites for web testing (and mobile testing with --mobile)").option("--mobile", "Also require a complete iOS or Android toolchain (exit 1 without one)").action((opts) => {
|
|
14364
15284
|
const report = runDoctor();
|
|
14365
|
-
printDoctorReport(report);
|
|
14366
|
-
process.exit(report.
|
|
15285
|
+
printDoctorReport(report, { mobile: opts.mobile === true });
|
|
15286
|
+
process.exit(doctorExitCode(report, { mobile: opts.mobile === true }));
|
|
14367
15287
|
});
|
|
14368
15288
|
program.command("setup-mobile").description("Install Appium and the iOS (XCUITest) + Android (UiAutomator2) drivers").action(async () => {
|
|
14369
15289
|
const { spawn: spawn2 } = await import("child_process");
|