@validate.qa/runner 1.0.103 → 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-YI3ZC2XI.mjs → chunk-6N4QCCEH.mjs} +2 -2
- package/dist/{chunk-CMM4DOXC.mjs → chunk-EBWZ7NGU.mjs} +11 -8
- package/dist/{chunk-OTKJZIOD.mjs → chunk-EKY6CN4Y.mjs} +659 -122
- package/dist/{chunk-CG75I3ZZ.mjs → chunk-MEE4DXRX.mjs} +13437 -1386
- package/dist/{chunk-WVLBQDFP.mjs → chunk-OXDU6ARE.mjs} +8 -2
- package/dist/{chunk-AAUHXGDS.mjs → chunk-PUMRAFJS.mjs} +37 -13
- package/dist/{chunk-BECCGMU4.mjs → chunk-TGVKABFN.mjs} +45 -3
- package/dist/{chunk-YIM746WO.mjs → chunk-TS4OUJVT.mjs} +1 -1
- package/dist/{chunk-NNJYKR3O.mjs → chunk-UR7KDOTP.mjs} +29 -6
- package/dist/{chunk-36AWDKPL.mjs → chunk-UZ67UBXH.mjs} +14 -3
- package/dist/{chunk-7SH3COMQ.mjs → chunk-VWE3SQ2Z.mjs} +1 -1
- package/dist/{chunk-TK5KEGNW.mjs → chunk-XXZZMG6Q.mjs} +15 -3
- package/dist/cli.js +20962 -7343
- package/dist/cli.mjs +1159 -245
- package/dist/credential-mcp-server.js +671 -127
- package/dist/credential-mcp-server.mjs +3 -3
- package/dist/discovery-mcp-server.js +710 -129
- package/dist/discovery-mcp-server.mjs +4 -4
- package/dist/dist-BXHLIG5H.mjs +17 -0
- package/dist/inbox-mcp-server.js +664 -123
- package/dist/inbox-mcp-server.mjs +3 -3
- package/dist/route-seed-worker.js +671 -124
- package/dist/route-seed-worker.mjs +3 -3
- package/dist/run-write-guard-preload.js +713 -125
- package/dist/run-write-guard-preload.mjs +4 -4
- package/dist/run-write-guard.js +713 -125
- 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-2G7YTNH5.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/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/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,
|
|
@@ -10589,7 +10721,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10589
10721
|
const hasFixFailed = enrichedSteps.some((s) => s.verdict === "fix_failed");
|
|
10590
10722
|
const hasPass = enrichedSteps.some((s) => s.verdict === "pass");
|
|
10591
10723
|
const verifyPassed = stepCount > 0 ? finalVerifyResult?.passed === true && hasPass && !hasFixFailed && !allAppBug : finalVerifyResult?.passed === true;
|
|
10592
|
-
const partialSkipError = verifyPassed ? (0,
|
|
10724
|
+
const partialSkipError = verifyPassed ? (0, import_runner_core8.apiHealPartialSkipError)(enrichedSteps) : void 0;
|
|
10593
10725
|
const passed = verifyPassed && partialSkipError === void 0;
|
|
10594
10726
|
let composedError;
|
|
10595
10727
|
if (!passed) {
|
|
@@ -10607,11 +10739,21 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10607
10739
|
let apiHealStatus = passed ? "PASSED" : "FAILED";
|
|
10608
10740
|
let apiHealError = composedError;
|
|
10609
10741
|
if (!passed) {
|
|
10610
|
-
if (finalUnfixableReason?.startsWith(
|
|
10742
|
+
if (finalUnfixableReason?.startsWith(import_runner_core8.HEAL_AI_NO_ANSWER_ERROR_PREFIX) || (0, import_runner_core8.isHealAiQuotaError)(finalUnfixableReason)) {
|
|
10611
10743
|
apiHealStatus = "ERROR";
|
|
10612
10744
|
apiHealError = finalUnfixableReason;
|
|
10613
10745
|
} else if (isConfigLikeReason(classifyErrorMessage(composedError ?? null))) {
|
|
10614
|
-
|
|
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
|
+
}
|
|
10615
10757
|
} else {
|
|
10616
10758
|
const rlDetection = (0, import_rate_limit_detector.detectRateLimitBlock)({
|
|
10617
10759
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
@@ -10645,10 +10787,10 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10645
10787
|
}
|
|
10646
10788
|
}
|
|
10647
10789
|
if (apiHealStatus === "FAILED") {
|
|
10648
|
-
const cold = (0,
|
|
10790
|
+
const cold = (0, import_runner_core9.apiHealColdSessionRelabel)("verified", {
|
|
10649
10791
|
tags: run2.tags ?? [],
|
|
10650
10792
|
authStateExpected: apiHealAuthPlan.needsCookieState,
|
|
10651
|
-
authStateInjected: (0,
|
|
10793
|
+
authStateInjected: (0, import_runner_core9.storageStateDelivered)(apiHealStorageStatePath, finalVerifyResult),
|
|
10652
10794
|
needsStandingHeaders: apiHealAuthPlan.needsStandingHeaders,
|
|
10653
10795
|
standingHeadersInjected: !!finalVerifyResult?.authDelivery?.standingHeadersInjected,
|
|
10654
10796
|
error: apiHealError ?? finalVerifyResult?.error,
|
|
@@ -10711,11 +10853,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10711
10853
|
completionTokens: info.completionTokens,
|
|
10712
10854
|
totalTokens: info.totalTokens,
|
|
10713
10855
|
...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
|
|
10714
|
-
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) }
|
|
10715
10857
|
});
|
|
10716
10858
|
};
|
|
10717
10859
|
const envOverride = (process.env.USE_GROK_PER_TEST_HEAL ?? "").trim().toLowerCase();
|
|
10718
|
-
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";
|
|
10719
10861
|
const healOpts = {
|
|
10720
10862
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
10721
10863
|
tags: run2.tags ?? [],
|
|
@@ -10738,19 +10880,19 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10738
10880
|
// Whole-heal deadline: the runner's heal deadline minus a margin for the result
|
|
10739
10881
|
// post (heal-deadline.ts). The healer stops both phases inside it and returns a
|
|
10740
10882
|
// HEAL_TIMEOUT: result instead of dying as a reclaimed task.
|
|
10741
|
-
deadlineAt: (0,
|
|
10883
|
+
deadlineAt: (0, import_runner_core8.healDeadlineAt)(startTime)
|
|
10742
10884
|
};
|
|
10743
10885
|
if (useGrokHeal) {
|
|
10744
10886
|
const source = envOverride === "true" ? "env USE_GROK_PER_TEST_HEAL=true" : `healerModel=${run2.healerModel ?? "null"}`;
|
|
10745
10887
|
log.info(`Heal engine: GROK (${source})`);
|
|
10746
10888
|
}
|
|
10747
10889
|
const grokHealLogStream = useGrokHeal ? createHealLogStreamer(opts.serverUrl, headers, run2.runId) : null;
|
|
10748
|
-
const httpFallback = run2.healerFallbackModel && (0,
|
|
10890
|
+
const httpFallback = run2.healerFallbackModel && (0, import_runner_core10.getAgenticEngine)(run2.healerFallbackModel) === "native" ? run2.healerFallbackModel : null;
|
|
10749
10891
|
let result2;
|
|
10750
10892
|
if (useGrokHeal) {
|
|
10751
10893
|
try {
|
|
10752
10894
|
const grokLogin = await (0, import_fetch_cli_subscription.fetchCliSubscription)({ serverUrl: opts.serverUrl, runId: run2.runId, headers, cli: "grok-build", onLog: grokHealLogStream?.onLog });
|
|
10753
|
-
result2 = await (0,
|
|
10895
|
+
result2 = await (0, import_runner_core10.executeGrokPerTestHeal)(run2.playwrightCode, run2.steps, run2.baseUrl, {
|
|
10754
10896
|
...healOpts,
|
|
10755
10897
|
subscriptionSecret: grokLogin?.value,
|
|
10756
10898
|
saveRefreshedLogin: grokLogin?.saveRefreshed,
|
|
@@ -10795,10 +10937,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10795
10937
|
console.warn(`[runner]${auditDrainError}`);
|
|
10796
10938
|
}
|
|
10797
10939
|
const relabelSteps = result2.relabelEvidenceSteps ?? result2.stepResults;
|
|
10798
|
-
const
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
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)({
|
|
10802
10946
|
passed: result2.passed,
|
|
10803
10947
|
isConfigIssue,
|
|
10804
10948
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
@@ -10813,7 +10957,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10813
10957
|
healError = rateLimitRelabel.error;
|
|
10814
10958
|
log.warn(rateLimitRelabel.logLine);
|
|
10815
10959
|
}
|
|
10816
|
-
const loginRelabel = (0,
|
|
10960
|
+
const loginRelabel = (0, import_runner_core9.uiHealLoginRelabel)({
|
|
10817
10961
|
status: healStatus,
|
|
10818
10962
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
10819
10963
|
tags: run2.tags ?? [],
|
|
@@ -10888,7 +11032,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10888
11032
|
signal
|
|
10889
11033
|
});
|
|
10890
11034
|
const overall = (0, import_regression_runner.regressionSuiteOverall)(result2);
|
|
10891
|
-
(0,
|
|
11035
|
+
(0, import_runner_core10.throwIfRunAborted)();
|
|
10892
11036
|
await postRegressionResult(opts.serverUrl, headers, run2.runId, {
|
|
10893
11037
|
overall,
|
|
10894
11038
|
logs: result2.logs,
|
|
@@ -10918,9 +11062,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10918
11062
|
`${opts.serverUrl}/api/runner/runs/${run2.runId}/ai-audit-batch`,
|
|
10919
11063
|
headers
|
|
10920
11064
|
);
|
|
10921
|
-
const suiteQuotaFallback = (0,
|
|
11065
|
+
const suiteQuotaFallback = (0, import_runner_core8.createQuotaFallbackState)(run2.quotaFallbackModel);
|
|
10922
11066
|
for (const failedTest of failedTests) {
|
|
10923
|
-
(0,
|
|
11067
|
+
(0, import_runner_core10.throwIfRunAborted)();
|
|
10924
11068
|
const testStart = Date.now();
|
|
10925
11069
|
log.info(` Healing: ${failedTest.name ?? failedTest.testCaseId}`);
|
|
10926
11070
|
const isApiTest = Array.isArray(failedTest.tags) && failedTest.tags.includes("@api");
|
|
@@ -10933,7 +11077,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10933
11077
|
signal: AbortSignal.timeout(6e4)
|
|
10934
11078
|
});
|
|
10935
11079
|
const healBody = healRes.ok ? await healRes.json() : null;
|
|
10936
|
-
if (healBody?.verdict === "unfixable" && (0,
|
|
11080
|
+
if (healBody?.verdict === "unfixable" && (0, import_runner_core8.isHealAiQuotaError)(healBody.reason)) {
|
|
10937
11081
|
results.push({
|
|
10938
11082
|
testCaseId: failedTest.testCaseId,
|
|
10939
11083
|
status: "unfixable",
|
|
@@ -10953,7 +11097,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10953
11097
|
authStandingMode: failedTestPlan.standingHeadersMode,
|
|
10954
11098
|
validateContext: callbackContext
|
|
10955
11099
|
});
|
|
10956
|
-
const verdict = (0,
|
|
11100
|
+
const verdict = (0, import_runner_core8.apiHealVerifyVerdict)(verifyResult);
|
|
10957
11101
|
results.push({
|
|
10958
11102
|
testCaseId: failedTest.testCaseId,
|
|
10959
11103
|
status: verdict.fixed ? "fixed" : "unfixable",
|
|
@@ -10999,12 +11143,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10999
11143
|
completionTokens: info.completionTokens,
|
|
11000
11144
|
totalTokens: info.totalTokens,
|
|
11001
11145
|
...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
|
|
11002
|
-
metadata: { groupId: info.groupId, testCaseId: failedTest.testCaseId, ...(0,
|
|
11146
|
+
metadata: { groupId: info.groupId, testCaseId: failedTest.testCaseId, ...(0, import_runner_core10.cliAuditMetadata)(info) }
|
|
11003
11147
|
});
|
|
11004
11148
|
};
|
|
11005
11149
|
try {
|
|
11006
11150
|
const envOverrideSuite = (process.env.USE_GROK_PER_TEST_HEAL ?? "").trim().toLowerCase();
|
|
11007
|
-
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";
|
|
11008
11152
|
const suiteHealOpts = {
|
|
11009
11153
|
testName: failedTest.name ?? failedTest.testCaseId,
|
|
11010
11154
|
tags: failedTest.tags ?? [],
|
|
@@ -11026,12 +11170,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11026
11170
|
liveBrowserHandle,
|
|
11027
11171
|
runNativeTest: runNativeWithLiveBrowser
|
|
11028
11172
|
};
|
|
11029
|
-
const suiteHttpFallback = run2.healerFallbackModel && (0,
|
|
11173
|
+
const suiteHttpFallback = run2.healerFallbackModel && (0, import_runner_core10.getAgenticEngine)(run2.healerFallbackModel) === "native" ? run2.healerFallbackModel : null;
|
|
11030
11174
|
let healResult;
|
|
11031
11175
|
if (useGrokHealSuite) {
|
|
11032
11176
|
try {
|
|
11033
11177
|
const grokLogin = await (0, import_fetch_cli_subscription.fetchCliSubscription)({ serverUrl: opts.serverUrl, runId: run2.runId, headers, cli: "grok-build", onLog: log.warn });
|
|
11034
|
-
healResult = await (0,
|
|
11178
|
+
healResult = await (0, import_runner_core10.executeGrokPerTestHeal)(
|
|
11035
11179
|
failedTest.playwrightCode,
|
|
11036
11180
|
failedTest.steps ?? [],
|
|
11037
11181
|
run2.baseUrl,
|
|
@@ -11071,20 +11215,21 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11071
11215
|
});
|
|
11072
11216
|
log.ok(` Fixed: ${failedTest.name}`);
|
|
11073
11217
|
} else {
|
|
11074
|
-
const quotaStopped = (0,
|
|
11075
|
-
const
|
|
11076
|
-
const isAppBug = !
|
|
11077
|
-
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));
|
|
11078
11223
|
const status = isAppBug ? "app-bug" : isConfigIssue ? "config-issue" : "unfixable";
|
|
11079
|
-
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";
|
|
11080
11225
|
results.push({
|
|
11081
11226
|
testCaseId: failedTest.testCaseId,
|
|
11082
11227
|
status,
|
|
11083
|
-
error: healResult.error,
|
|
11228
|
+
error: configIssueRefusal?.error ?? healResult.error,
|
|
11084
11229
|
diagnosis: diagnosisMsg,
|
|
11085
11230
|
duration: Date.now() - testStart
|
|
11086
11231
|
});
|
|
11087
|
-
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}`);
|
|
11088
11233
|
}
|
|
11089
11234
|
} catch (err) {
|
|
11090
11235
|
results.push({
|
|
@@ -11097,7 +11242,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11097
11242
|
log.fail(` Heal crashed: ${failedTest.name}`);
|
|
11098
11243
|
}
|
|
11099
11244
|
}
|
|
11100
|
-
(0,
|
|
11245
|
+
(0, import_runner_core10.throwIfRunAborted)();
|
|
11101
11246
|
const lastResult = results[results.length - 1];
|
|
11102
11247
|
if (lastResult) {
|
|
11103
11248
|
fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/live-suite-progress`, {
|
|
@@ -11126,7 +11271,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11126
11271
|
auditDrainWarning = drainErr instanceof Error ? drainErr.message : String(drainErr);
|
|
11127
11272
|
console.warn(`[runner] ${auditDrainWarning}`);
|
|
11128
11273
|
}
|
|
11129
|
-
(0,
|
|
11274
|
+
(0, import_runner_core10.throwIfRunAborted)();
|
|
11130
11275
|
const liveRes = await postLiveSuiteResult(opts.serverUrl, headers, run2.runId, {
|
|
11131
11276
|
overall,
|
|
11132
11277
|
duration: totalDuration,
|
|
@@ -11301,7 +11446,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11301
11446
|
}
|
|
11302
11447
|
return await res.json();
|
|
11303
11448
|
};
|
|
11304
|
-
ctx.smsReserve = (0,
|
|
11449
|
+
ctx.smsReserve = (0, import_runner_core10.createHttpSmsReserveTransport)({ serverUrl: opts.serverUrl, headers, runId: run2.runId });
|
|
11305
11450
|
}
|
|
11306
11451
|
const canonicalBase = await (0, import_base_url_check.canonicalDiscoveryBaseUrl)(
|
|
11307
11452
|
{ runMode, baseUrl: ctx.baseUrl, native: ctx.medium === "IOS_NATIVE" || ctx.medium === "ANDROID_NATIVE" || run2.framework === "APPIUM" }
|
|
@@ -11363,7 +11508,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11363
11508
|
...info.exitReason ? { exitReason: info.exitReason } : {},
|
|
11364
11509
|
// Engine cost, engine id, step, cache + reasoning tokens: same helper as the server's
|
|
11365
11510
|
// Fargate path (execute-run.ts), so the two rows agree.
|
|
11366
|
-
...(0,
|
|
11511
|
+
...(0, import_runner_core10.cliAuditMetadata)(info),
|
|
11367
11512
|
...ledgerAudit ? {
|
|
11368
11513
|
ledger: ledgerAudit.entries,
|
|
11369
11514
|
ledgerEntryCount: ledgerAudit.originalLength,
|
|
@@ -11398,12 +11543,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11398
11543
|
}).catch(() => {
|
|
11399
11544
|
});
|
|
11400
11545
|
};
|
|
11401
|
-
const onScreenshot = async (route2, base64, visual) => {
|
|
11546
|
+
const onScreenshot = async (route2, base64, visual, capture) => {
|
|
11402
11547
|
try {
|
|
11403
11548
|
const resp = await fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/page-screenshot`, {
|
|
11404
11549
|
method: "POST",
|
|
11405
11550
|
headers: { ...headers, "Content-Type": "application/json" },
|
|
11406
|
-
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 } : {} }),
|
|
11407
11552
|
signal: AbortSignal.timeout(15e3)
|
|
11408
11553
|
});
|
|
11409
11554
|
if (!resp.ok) return null;
|
|
@@ -11672,7 +11817,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11672
11817
|
requestLiveBrowserHeartbeat(true);
|
|
11673
11818
|
mobileOnLog(`Mobile discovery (${ctx.mode}) \u2014 ${platform3} / ${mobileTarget.appId}`);
|
|
11674
11819
|
setExecutorBusy(true);
|
|
11675
|
-
let
|
|
11820
|
+
let session2 = null;
|
|
11676
11821
|
let capture = null;
|
|
11677
11822
|
let discoveryTeardownStarted = false;
|
|
11678
11823
|
try {
|
|
@@ -11693,7 +11838,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11693
11838
|
discoverySessionTarget,
|
|
11694
11839
|
onDeviceSelected ? { onDeviceSelected } : void 0
|
|
11695
11840
|
);
|
|
11696
|
-
|
|
11841
|
+
session2 = await openDiscoverySession(
|
|
11697
11842
|
async (device) => {
|
|
11698
11843
|
discoverySessionTarget.assignedDeviceId = device.deviceId;
|
|
11699
11844
|
liveBrowserHandle.patch({ deviceId: device.deviceId, deviceName: device.name });
|
|
@@ -11714,18 +11859,18 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11714
11859
|
}
|
|
11715
11860
|
}
|
|
11716
11861
|
);
|
|
11717
|
-
mobileOnLog(`Appium session created: ${
|
|
11718
|
-
attachMirrorWithRelay(
|
|
11862
|
+
mobileOnLog(`Appium session created: ${session2.sessionId} on device ${session2.deviceId}`);
|
|
11863
|
+
attachMirrorWithRelay(session2.sessionId, platform3, session2.deviceId);
|
|
11719
11864
|
const driver = new MobileBridgeDriver({
|
|
11720
|
-
sessionId:
|
|
11865
|
+
sessionId: session2.sessionId,
|
|
11721
11866
|
platform: platform3,
|
|
11722
|
-
appId:
|
|
11723
|
-
deviceId:
|
|
11867
|
+
appId: session2.appId,
|
|
11868
|
+
deviceId: session2.deviceId,
|
|
11724
11869
|
// Deterministic-state authorization: Android emulators / iOS
|
|
11725
11870
|
// simulators only (VALIDATEQA_MOBILE_APP_RESET overrides the
|
|
11726
11871
|
// Android default) — a personal physical device must never have
|
|
11727
11872
|
// its app data wiped.
|
|
11728
|
-
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3,
|
|
11873
|
+
allowAppReset: mobileTarget.resetPolicy !== "PRESERVE_APP_DATA" && shouldResetMobileAppData(platform3, session2.deviceId),
|
|
11729
11874
|
// Dual-device discovery + long AI turns can lose WDA/UIA2 more than
|
|
11730
11875
|
// once per run. One recovery was too tight — the next snapshot
|
|
11731
11876
|
// failed as "no UI tree" after a single WDA death.
|
|
@@ -11734,13 +11879,13 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11734
11879
|
if (discoveryTeardownStarted) {
|
|
11735
11880
|
throw new Error("discovery teardown already started \u2014 not recreating the Appium session");
|
|
11736
11881
|
}
|
|
11737
|
-
const lost =
|
|
11882
|
+
const lost = session2;
|
|
11738
11883
|
mobileOnLog(
|
|
11739
11884
|
`[appium] discovery session lost (${cause.message}); recreating on the leased device`
|
|
11740
11885
|
);
|
|
11741
11886
|
if (lost) {
|
|
11742
11887
|
detachMirrorSession(lost.deviceId);
|
|
11743
|
-
|
|
11888
|
+
session2 = null;
|
|
11744
11889
|
try {
|
|
11745
11890
|
await lost.stop();
|
|
11746
11891
|
} catch {
|
|
@@ -11754,7 +11899,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11754
11899
|
}
|
|
11755
11900
|
throw new Error("discovery teardown started during session recovery \u2014 replacement session closed");
|
|
11756
11901
|
}
|
|
11757
|
-
|
|
11902
|
+
session2 = replacement;
|
|
11758
11903
|
attachMirrorWithRelay(replacement.sessionId, platform3, replacement.deviceId);
|
|
11759
11904
|
liveBrowserHandle.patch({
|
|
11760
11905
|
deviceId: replacement.deviceId,
|
|
@@ -11818,7 +11963,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11818
11963
|
const apiCalls = stopped;
|
|
11819
11964
|
const captureHealth = activeCapture.health();
|
|
11820
11965
|
if (discoveryResult) {
|
|
11821
|
-
discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult,
|
|
11966
|
+
discoveryResult.apiCalls = apiCalls.length ? attachMobileDiscoveryNetworkContext(apiCalls, discoveryResult, session2?.appId ?? mobileTarget.appId) : void 0;
|
|
11822
11967
|
const priorHealth = discoveryResult.healthObservations;
|
|
11823
11968
|
const healthObject = {
|
|
11824
11969
|
consoleErrors: priorHealth?.consoleErrors ?? [],
|
|
@@ -11833,9 +11978,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11833
11978
|
mobileOnLog(`[mobile-net] capture stop failed: ${capErr instanceof Error ? capErr.message : String(capErr)}`);
|
|
11834
11979
|
}
|
|
11835
11980
|
}
|
|
11836
|
-
if (
|
|
11837
|
-
detachMirrorSession(
|
|
11838
|
-
const closing =
|
|
11981
|
+
if (session2) {
|
|
11982
|
+
detachMirrorSession(session2.deviceId);
|
|
11983
|
+
const closing = session2;
|
|
11839
11984
|
try {
|
|
11840
11985
|
const stopped = await timeboxed(closing.stop());
|
|
11841
11986
|
if (stopped === TEARDOWN_STEP_TIMEOUT) {
|
|
@@ -11908,7 +12053,7 @@ ${line}`)
|
|
|
11908
12053
|
};
|
|
11909
12054
|
} else if (ctx.frameworkImport?.kind === "app-map") {
|
|
11910
12055
|
log.info("Building the app map.");
|
|
11911
|
-
discoveryResult = await (0,
|
|
12056
|
+
discoveryResult = await (0, import_runner_core10.runAppMapOnRunner)(ctx, { onLog: (line) => log.info(line) });
|
|
11912
12057
|
} else if (ctx.frameworkImport?.kind === "repo-enrichment") {
|
|
11913
12058
|
const frameworkImport = ctx.frameworkImport;
|
|
11914
12059
|
const primaryCli = isRepositoryCli(frameworkImport.repoHuntCli) ? frameworkImport.repoHuntCli : "claude-code";
|
|
@@ -11933,7 +12078,7 @@ ${line}`)
|
|
|
11933
12078
|
})
|
|
11934
12079
|
});
|
|
11935
12080
|
} else {
|
|
11936
|
-
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ?
|
|
12081
|
+
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ? import_runner_core10.runRepoAnalysisOnRunner : import_framework_import_runner.runFrameworkImportOnRunner;
|
|
11937
12082
|
discoveryResult = await runRepoImport(ctx, { onTestGenerated, acquireLoginBrowser, onAudit: discoveryAudit }, {
|
|
11938
12083
|
archiveUrl: ctx.frameworkImport?.source === "upload" ? `${opts.serverUrl}/api/runner/runs/${run2.runId}/import-archive` : void 0,
|
|
11939
12084
|
archiveAuthHeader: headers.Authorization,
|
|
@@ -11995,7 +12140,7 @@ ${line}`)
|
|
|
11995
12140
|
}
|
|
11996
12141
|
const resolveCliSubscription = (engine, logLine) => (0, import_fetch_cli_subscription.fetchCliSubscription)({ serverUrl: opts.serverUrl, runId: run2.runId, headers, cli: engine, onLog: logLine });
|
|
11997
12142
|
const claimNonce = claimNonceFor(run2.runId);
|
|
11998
|
-
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)({
|
|
11999
12144
|
serverUrl: opts.serverUrl,
|
|
12000
12145
|
runId: run2.runId,
|
|
12001
12146
|
claimEpoch: run2.claimEpoch,
|
|
@@ -12015,7 +12160,7 @@ ${line}`)
|
|
|
12015
12160
|
if (ctx.repoBrief) discoveryResult.repoBrief = ctx.repoBrief;
|
|
12016
12161
|
if (ctx.repoRouteManifest) discoveryResult.repoRouteManifest = ctx.repoRouteManifest;
|
|
12017
12162
|
}
|
|
12018
|
-
discoveryResult = (0,
|
|
12163
|
+
discoveryResult = (0, import_runner_core10.redactDiscoveryCaptureNeedles)(discoveryResult, {
|
|
12019
12164
|
...run2.testVariables,
|
|
12020
12165
|
// The saved sign-in's values, with every one it rotated to during the run.
|
|
12021
12166
|
...savedSignIn?.loaded ? savedSignIn.secrets() : {}
|
|
@@ -12173,7 +12318,7 @@ ${line}`)
|
|
|
12173
12318
|
completionTokens: info.completionTokens,
|
|
12174
12319
|
totalTokens: info.totalTokens,
|
|
12175
12320
|
...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
|
|
12176
|
-
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) }
|
|
12177
12322
|
});
|
|
12178
12323
|
};
|
|
12179
12324
|
const logBuffer = [];
|
|
@@ -12390,7 +12535,7 @@ ${line}`)
|
|
|
12390
12535
|
completionTokens: info.completionTokens,
|
|
12391
12536
|
totalTokens: info.totalTokens,
|
|
12392
12537
|
...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
|
|
12393
|
-
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) }
|
|
12394
12539
|
});
|
|
12395
12540
|
},
|
|
12396
12541
|
runLiveHeal: async (a) => {
|
|
@@ -12732,7 +12877,7 @@ ${result2.logs}`);
|
|
|
12732
12877
|
if (storageStatePath) {
|
|
12733
12878
|
log.info(`Using auth state for project ${projectId}`);
|
|
12734
12879
|
}
|
|
12735
|
-
const authSession = (0,
|
|
12880
|
+
const authSession = (0, import_runner_core9.sharedAuthSessionStamp)(storageStatePath, captureAuth, sharedAuthInjectEnabled());
|
|
12736
12881
|
const runAuthPath = captureAuth ? authPaths?.capturePath : void 0;
|
|
12737
12882
|
const result = await (async () => {
|
|
12738
12883
|
try {
|
|
@@ -12791,16 +12936,17 @@ ${result2.logs}`);
|
|
|
12791
12936
|
runStatus = "ERROR";
|
|
12792
12937
|
log.warn(`\u26A0 CONFIG_ISSUE from native execution - posting ERROR (not FAILED) to skip heal/quarantine`);
|
|
12793
12938
|
}
|
|
12794
|
-
const authRelabel = (0,
|
|
12939
|
+
const authRelabel = (0, import_runner_core9.authReadinessRelabel)({
|
|
12795
12940
|
status: runStatus,
|
|
12796
12941
|
error: runError,
|
|
12797
12942
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
12798
12943
|
tags: run2.tags ?? [],
|
|
12944
|
+
code: run2.playwrightCode,
|
|
12799
12945
|
logs: result.logs,
|
|
12800
12946
|
stepResults: result.stepResults,
|
|
12801
12947
|
apiCalls: result.apiCalls,
|
|
12802
12948
|
authStateExpected,
|
|
12803
|
-
authStateInjected: (0,
|
|
12949
|
+
authStateInjected: (0, import_runner_core9.storageStateDelivered)(storageStatePath, result),
|
|
12804
12950
|
needsStandingHeaders: normalRunAuthPlan.needsStandingHeaders,
|
|
12805
12951
|
standingHeadersInjected: !!result.authDelivery?.standingHeadersInjected
|
|
12806
12952
|
});
|
|
@@ -12809,7 +12955,7 @@ ${result2.logs}`);
|
|
|
12809
12955
|
runError = authRelabel.error;
|
|
12810
12956
|
log.warn(authRelabel.logLine);
|
|
12811
12957
|
}
|
|
12812
|
-
(0,
|
|
12958
|
+
(0, import_runner_core10.throwIfRunAborted)();
|
|
12813
12959
|
await postRunResult({
|
|
12814
12960
|
status: runStatus,
|
|
12815
12961
|
stepResults: result.stepResults,
|
|
@@ -12832,7 +12978,7 @@ ${result2.logs}`);
|
|
|
12832
12978
|
if (result.passed) {
|
|
12833
12979
|
log.ok(`PASSED (${duration}ms)${result.suspiciousPass ? ` \u26A0 suspicious-pass (viewport ${Math.round(result.suspiciousPass.emptyRatio * 100)}% blank)` : ""}`);
|
|
12834
12980
|
} else if (runStatus === "ERROR") {
|
|
12835
|
-
log.warn(
|
|
12981
|
+
log.warn(blockedRunLogLine(runError, duration));
|
|
12836
12982
|
} else {
|
|
12837
12983
|
log.fail(`FAILED (${duration}ms)${result.error ? ` - ${result.error}` : ""}`);
|
|
12838
12984
|
if (result.logs) {
|
|
@@ -12843,14 +12989,14 @@ ${result2.logs}`);
|
|
|
12843
12989
|
}
|
|
12844
12990
|
} catch (err) {
|
|
12845
12991
|
if (err instanceof RunNoLongerActiveError) throw err;
|
|
12846
|
-
const abortOutcome = (0,
|
|
12992
|
+
const abortOutcome = (0, import_runner_core10.runAbortOutcome)(err, signal);
|
|
12847
12993
|
if (abortOutcome === "discard") {
|
|
12848
|
-
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`);
|
|
12849
12995
|
}
|
|
12850
12996
|
if (abortOutcome === "interrupt") {
|
|
12851
12997
|
await postRunResult({
|
|
12852
12998
|
status: "ERROR",
|
|
12853
|
-
error:
|
|
12999
|
+
error: import_runner_core10.RUNNER_SHUTDOWN_INTERRUPT_ERROR,
|
|
12854
13000
|
duration: Date.now() - startTime
|
|
12855
13001
|
});
|
|
12856
13002
|
throw err;
|
|
@@ -12967,13 +13113,6 @@ async function startRunner(opts) {
|
|
|
12967
13113
|
6e4,
|
|
12968
13114
|
envInt("VALIDATE_RUNNER_VERSION_CHECK_MS", 15 * 6e4)
|
|
12969
13115
|
);
|
|
12970
|
-
const exitForUpgrade = (latestVersion, installed) => {
|
|
12971
|
-
console.error(formatUpgradeInstructions(latestVersion));
|
|
12972
|
-
console.error(` Installed: ${installed}`);
|
|
12973
|
-
console.error(` Required: ${latestVersion} (or newer)`);
|
|
12974
|
-
console.error("");
|
|
12975
|
-
process.exit(1);
|
|
12976
|
-
};
|
|
12977
13116
|
let drainingForUpgrade = false;
|
|
12978
13117
|
const deferUpgradeWhileBusy = () => {
|
|
12979
13118
|
if (upgradeAction(activeRuns) === "exit") return false;
|
|
@@ -13054,9 +13193,11 @@ async function startRunner(opts) {
|
|
|
13054
13193
|
await browserPool.init();
|
|
13055
13194
|
log.ok(`Browser pool: ${poolSize} shared browsers ready`);
|
|
13056
13195
|
} catch (err) {
|
|
13057
|
-
|
|
13058
|
-
log.fail(
|
|
13059
|
-
|
|
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);
|
|
13060
13201
|
}
|
|
13061
13202
|
}
|
|
13062
13203
|
const SHUTDOWN_FINISH_MS = Math.max(0, envInt("RUNNER_SHUTDOWN_FINISH_MS", 5e3));
|
|
@@ -13328,7 +13469,7 @@ async function startRunner(opts) {
|
|
|
13328
13469
|
}
|
|
13329
13470
|
});
|
|
13330
13471
|
};
|
|
13331
|
-
const removeSupersededRunFetchFence = (0,
|
|
13472
|
+
const removeSupersededRunFetchFence = (0, import_runner_core10.installSupersededRunFetchFence)(serverUrl);
|
|
13332
13473
|
const heartbeat = setInterval(sendHeartbeat, 3e4);
|
|
13333
13474
|
sendHeartbeat();
|
|
13334
13475
|
let shuttingDown = false;
|
|
@@ -13340,7 +13481,7 @@ async function startRunner(opts) {
|
|
|
13340
13481
|
headers: { ...headers, "Content-Type": "application/json" },
|
|
13341
13482
|
body: JSON.stringify(withClaimEpoch({
|
|
13342
13483
|
status: "ERROR",
|
|
13343
|
-
error:
|
|
13484
|
+
error: import_runner_core10.RUNNER_SHUTDOWN_INTERRUPT_ERROR
|
|
13344
13485
|
}, claimEpoch)),
|
|
13345
13486
|
signal: AbortSignal.timeout(5e3)
|
|
13346
13487
|
}));
|
|
@@ -13356,7 +13497,7 @@ async function startRunner(opts) {
|
|
|
13356
13497
|
setTimeout(() => {
|
|
13357
13498
|
console.error("Shutdown grace elapsed \u2014 forcing exit");
|
|
13358
13499
|
process.exit(0);
|
|
13359
|
-
}, (0,
|
|
13500
|
+
}, (0, import_runner_core10.shutdownForceExitMs)(SHUTDOWN_FINISH_MS, SHUTDOWN_GRACE_MS)).unref();
|
|
13360
13501
|
clearInterval(heartbeat);
|
|
13361
13502
|
heartbeatKeepalive.stop();
|
|
13362
13503
|
if (liveBrowserHeartbeatTimer) {
|
|
@@ -13374,7 +13515,7 @@ async function startRunner(opts) {
|
|
|
13374
13515
|
if (claimPending) {
|
|
13375
13516
|
await Promise.race([claimPending, new Promise((resolve3) => setTimeout(resolve3, SHUTDOWN_FINISH_MS))]);
|
|
13376
13517
|
}
|
|
13377
|
-
await (0,
|
|
13518
|
+
await (0, import_runner_core10.drainAbortedRuns)({
|
|
13378
13519
|
finishMs: SHUTDOWN_FINISH_MS,
|
|
13379
13520
|
graceMs: SHUTDOWN_GRACE_MS,
|
|
13380
13521
|
inFlight: activeWebRuns,
|
|
@@ -13391,7 +13532,7 @@ async function startRunner(opts) {
|
|
|
13391
13532
|
await stopAppiumServer();
|
|
13392
13533
|
}
|
|
13393
13534
|
await browserPool.shutdown();
|
|
13394
|
-
await (0,
|
|
13535
|
+
await (0, import_runner_core11.closeLocalGoogleBroker)().catch(() => {
|
|
13395
13536
|
});
|
|
13396
13537
|
removeSupersededRunFetchFence();
|
|
13397
13538
|
log.info("Runner stopped.");
|
|
@@ -13455,7 +13596,7 @@ async function startRunner(opts) {
|
|
|
13455
13596
|
const abortSupersededRun = (runId, status, serverEpoch) => {
|
|
13456
13597
|
const controller = activeWebRuns.get(runId);
|
|
13457
13598
|
const already = !controller || controller.signal.aborted;
|
|
13458
|
-
if (controller) (0,
|
|
13599
|
+
if (controller) (0, import_runner_core10.abortRun)(controller, "superseded", runId);
|
|
13459
13600
|
activeMobileRuns.get(runId)?.triggerAbort(`was superseded by a newer claim (epoch ${serverEpoch ?? "?"}, status ${status})`);
|
|
13460
13601
|
if (!already) {
|
|
13461
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.`);
|
|
@@ -13471,7 +13612,7 @@ async function startRunner(opts) {
|
|
|
13471
13612
|
(runId, status) => {
|
|
13472
13613
|
const controller = activeWebRuns.get(runId);
|
|
13473
13614
|
const already = !controller || controller.signal.aborted;
|
|
13474
|
-
if (controller) (0,
|
|
13615
|
+
if (controller) (0, import_runner_core10.abortRun)(controller, "cancelled", runId);
|
|
13475
13616
|
activeMobileRuns.get(runId)?.triggerAbort(`was cancelled server-side (status ${status})`);
|
|
13476
13617
|
if (!already) log.warn(`Run ${runId} was cancelled server-side (status ${status}) \u2014 stopping it.`);
|
|
13477
13618
|
}
|
|
@@ -13671,6 +13812,7 @@ async function startRunner(opts) {
|
|
|
13671
13812
|
const applyRelease = () => {
|
|
13672
13813
|
claimedRunEpochs.delete(run2.runId);
|
|
13673
13814
|
claimNonces.delete(run2.runId);
|
|
13815
|
+
if (activeWebRuns.get(run2.runId) === runAbort) activeWebRuns.delete(run2.runId);
|
|
13674
13816
|
activeRuns--;
|
|
13675
13817
|
decrementType(runMode);
|
|
13676
13818
|
decrementProject(runProject);
|
|
@@ -13729,9 +13871,9 @@ async function startRunner(opts) {
|
|
|
13729
13871
|
runWatchdog.start();
|
|
13730
13872
|
}
|
|
13731
13873
|
activeWebRuns.set(run2.runId, runAbort);
|
|
13732
|
-
const runWritePolicy = (0,
|
|
13733
|
-
const runSignInSecrets = runWritePolicy ? (0,
|
|
13734
|
-
(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) => {
|
|
13735
13877
|
if (mobilePlatform) {
|
|
13736
13878
|
mobileHealHold.set(testCaseId, { expiry: Date.now() + MOBILE_HEAL_HOLD_MS, platform: mobilePlatform });
|
|
13737
13879
|
}
|
|
@@ -13739,11 +13881,11 @@ async function startRunner(opts) {
|
|
|
13739
13881
|
runsCompleted++;
|
|
13740
13882
|
}).catch((err) => {
|
|
13741
13883
|
const abortReason = runAbort.signal.reason;
|
|
13742
|
-
if ((0,
|
|
13884
|
+
if ((0, import_runner_core10.isRunAbortError)(abortReason) && abortReason.reason === "shutdown") {
|
|
13743
13885
|
log.info(`Run ${run2.runId} was interrupted by runner shutdown.`);
|
|
13744
13886
|
return;
|
|
13745
13887
|
}
|
|
13746
|
-
if ((0,
|
|
13888
|
+
if ((0, import_runner_core10.isSupersededRunAbort)(runAbort.signal)) {
|
|
13747
13889
|
log.info(`Run ${run2.runId} was superseded by a newer claim; this runner posted nothing more for it.`);
|
|
13748
13890
|
return;
|
|
13749
13891
|
}
|
|
@@ -13755,7 +13897,7 @@ async function startRunner(opts) {
|
|
|
13755
13897
|
log.warn(`Run ${run2.runId} crashed: ${msg}`);
|
|
13756
13898
|
}).finally(() => {
|
|
13757
13899
|
runSettled = true;
|
|
13758
|
-
activeWebRuns.delete(run2.runId);
|
|
13900
|
+
if (activeWebRuns.get(run2.runId) === runAbort) activeWebRuns.delete(run2.runId);
|
|
13759
13901
|
if (runWatchdog) runWatchdog.settle();
|
|
13760
13902
|
else applyRelease();
|
|
13761
13903
|
});
|
|
@@ -13815,12 +13957,61 @@ async function startRunner(opts) {
|
|
|
13815
13957
|
}
|
|
13816
13958
|
}
|
|
13817
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
|
+
|
|
13818
13977
|
// src/login-check.ts
|
|
13819
13978
|
var RUNNER_TOKEN_PREFIXES = ["urt_", "rt_"];
|
|
13820
13979
|
function runnerTokenShapeProblem(token) {
|
|
13821
13980
|
if (RUNNER_TOKEN_PREFIXES.some((prefix) => token.startsWith(prefix))) return null;
|
|
13822
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.";
|
|
13823
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
|
+
}
|
|
13824
14015
|
async function checkRunnerLogin(serverUrl, token, fetchImpl = fetch) {
|
|
13825
14016
|
let res;
|
|
13826
14017
|
try {
|
|
@@ -13828,17 +14019,388 @@ async function checkRunnerLogin(serverUrl, token, fetchImpl = fetch) {
|
|
|
13828
14019
|
} catch {
|
|
13829
14020
|
return { ok: false, message: `Could not reach ${serverUrl}` };
|
|
13830
14021
|
}
|
|
13831
|
-
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
|
+
}
|
|
13832
14026
|
if (res.status === 401 || res.status === 403) {
|
|
13833
14027
|
return { ok: false, message: "Invalid token - check your runner token (it may be revoked, or belong to another project)" };
|
|
13834
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
|
+
}
|
|
13835
14032
|
return { ok: false, message: `Server returned ${res.status} - credentials not saved, try again later` };
|
|
13836
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
|
+
}
|
|
13837
14193
|
|
|
13838
14194
|
// src/services/sign-in-capture.ts
|
|
13839
14195
|
var import_browser_config3 = __toESM(require_browser_config());
|
|
13840
14196
|
var import_google_local_signin = __toESM(require_google_local_signin());
|
|
13841
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
|
|
13842
14404
|
async function captureSignInInteractively(url, ready, opts = {}) {
|
|
13843
14405
|
const target = new URL(url);
|
|
13844
14406
|
let browser;
|
|
@@ -13847,47 +14409,316 @@ async function captureSignInInteractively(url, ready, opts = {}) {
|
|
|
13847
14409
|
} catch {
|
|
13848
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)() });
|
|
13849
14411
|
}
|
|
13850
|
-
return captureFromBrowser(browser, target, ready, opts.timeoutMs ?? SIGN_IN_TIMEOUT_MS);
|
|
14412
|
+
return captureFromBrowser(browser, target, ready, opts.timeoutMs ?? SIGN_IN_TIMEOUT_MS, opts);
|
|
13851
14413
|
}
|
|
13852
14414
|
var SIGN_IN_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
13853
|
-
|
|
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 = {}) {
|
|
13854
14588
|
let context;
|
|
14589
|
+
let watcher;
|
|
13855
14590
|
try {
|
|
13856
14591
|
context = await browser.newContext({ viewport: null });
|
|
13857
14592
|
const page = await context.newPage();
|
|
13858
14593
|
await page.goto(target.toString(), { waitUntil: "domcontentloaded" });
|
|
13859
|
-
|
|
13860
|
-
|
|
13861
|
-
|
|
13862
|
-
|
|
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?.();
|
|
13863
14612
|
try {
|
|
13864
|
-
|
|
13865
|
-
|
|
13866
|
-
|
|
13867
|
-
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
for (let i = 0; i < window.sessionStorage.length; i += 1) {
|
|
13871
|
-
const name = window.sessionStorage.key(i);
|
|
13872
|
-
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
|
+
}
|
|
13873
14619
|
}
|
|
13874
|
-
|
|
13875
|
-
|
|
13876
|
-
|
|
13877
|
-
|
|
13878
|
-
|
|
13879
|
-
|
|
13880
|
-
|
|
13881
|
-
|
|
13882
|
-
|
|
13883
|
-
|
|
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
|
+
}
|
|
13884
14634
|
} finally {
|
|
14635
|
+
watcher?.stop();
|
|
13885
14636
|
await context?.close().catch(() => {
|
|
13886
14637
|
});
|
|
13887
14638
|
await browser.close().catch(() => {
|
|
13888
14639
|
});
|
|
13889
14640
|
}
|
|
13890
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
|
+
}
|
|
13891
14722
|
function whenWindowClosed(browser, context) {
|
|
13892
14723
|
return new Promise((resolve3) => {
|
|
13893
14724
|
const watch = (page) => {
|
|
@@ -13946,7 +14777,22 @@ function closedTargetError() {
|
|
|
13946
14777
|
error.name = "TargetClosedError";
|
|
13947
14778
|
return error;
|
|
13948
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.";
|
|
13949
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) {
|
|
13950
14796
|
const doFetch = input.fetchImpl ?? fetch;
|
|
13951
14797
|
try {
|
|
13952
14798
|
const res = await doFetch(`${input.serverUrl.replace(/\/+$/, "")}/api/runner/saved-sign-in/capture`, {
|
|
@@ -13959,14 +14805,21 @@ async function uploadSignInCapture(input) {
|
|
|
13959
14805
|
storageState: input.capture.storageState,
|
|
13960
14806
|
userAgent: input.capture.userAgent,
|
|
13961
14807
|
locale: input.capture.locale,
|
|
13962
|
-
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 }
|
|
13963
14811
|
}),
|
|
13964
14812
|
signal: AbortSignal.timeout(6e4)
|
|
13965
14813
|
});
|
|
13966
14814
|
const body = await res.json().catch(() => ({}));
|
|
14815
|
+
if (!replace && res.status === 409 && body.reason === "live_sign_in_exists") return "live_sign_in_exists";
|
|
13967
14816
|
if (res.ok) return { ok: true, message: "Sign-in saved. Surveys and tests for this project will run signed in." };
|
|
13968
14817
|
if (res.status === 403) return { ok: false, message: body.error ?? "Only project admins can save a sign-in." };
|
|
13969
|
-
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." };
|
|
13970
14823
|
return { ok: false, message: `Could not save the sign-in (HTTP ${res.status}${body.error ? `: ${body.error}` : ""}).` };
|
|
13971
14824
|
} catch {
|
|
13972
14825
|
return { ok: false, message: "Could not reach the validate.qa server to save the sign-in." };
|
|
@@ -14032,6 +14885,43 @@ function shownAddress(url) {
|
|
|
14032
14885
|
return oneLine.replace(/^([a-z][a-z0-9+.-]*:\/\/)?\S*@/i, "$1");
|
|
14033
14886
|
}
|
|
14034
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
|
+
|
|
14035
14925
|
// src/supervisor-child-argv.ts
|
|
14036
14926
|
function stripRunnerTokenFromArgv(argv) {
|
|
14037
14927
|
const out = [];
|
|
@@ -14070,23 +14960,48 @@ program.name("validate-runner").description("validate.qa local test runner - exe
|
|
|
14070
14960
|
async function requireLatestRunner() {
|
|
14071
14961
|
await enforceRunnerUpToDate(pkg.version);
|
|
14072
14962
|
}
|
|
14073
|
-
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) => {
|
|
14074
14964
|
await requireLatestRunner();
|
|
14075
14965
|
const serverUrl = opts.server.replace(/\/+$/, "");
|
|
14076
|
-
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);
|
|
14077
14989
|
if (shapeProblem) {
|
|
14078
14990
|
console.error(`\u2717 ${shapeProblem}`);
|
|
14079
14991
|
process.exit(1);
|
|
14080
14992
|
}
|
|
14993
|
+
if (sendsTokenInPlaintext(serverUrl)) {
|
|
14994
|
+
console.warn("\u26A0 WARNING: Server URL uses HTTP, not HTTPS. Runner token will be sent in plaintext.");
|
|
14995
|
+
}
|
|
14081
14996
|
console.log(`Testing connection to ${serverUrl}...`);
|
|
14082
|
-
const check2 = await checkRunnerLogin(serverUrl,
|
|
14997
|
+
const check2 = await checkRunnerLogin(serverUrl, token);
|
|
14083
14998
|
if (!check2.ok) {
|
|
14084
14999
|
console.error(`\u2717 ${check2.message}`);
|
|
14085
15000
|
process.exit(1);
|
|
14086
15001
|
}
|
|
14087
15002
|
console.log("\u2713 Connected successfully");
|
|
14088
15003
|
saveCredentials({
|
|
14089
|
-
token
|
|
15004
|
+
token,
|
|
14090
15005
|
serverUrl,
|
|
14091
15006
|
projectId: opts.project
|
|
14092
15007
|
});
|
|
@@ -14171,17 +15086,10 @@ program.command("start").description("Start the local test runner").option("--to
|
|
|
14171
15086
|
}
|
|
14172
15087
|
await requireLatestRunner();
|
|
14173
15088
|
const saved = loadCredentials();
|
|
14174
|
-
const authToken = opts
|
|
14175
|
-
const serverUrl = (opts.server || process.env.SERVER_URL || saved?.serverUrl)?.replace(/\/+$/, "");
|
|
14176
|
-
const projectId = opts.project || process.env.PROJECT_ID || saved?.projectId;
|
|
15089
|
+
const { authToken, serverUrl, projectId, savedLoginSkipped } = resolveCredentials(opts, process.env, saved);
|
|
14177
15090
|
if (!authToken) {
|
|
14178
|
-
console.error(
|
|
14179
|
-
console.error(
|
|
14180
|
-
console.error("Steps:");
|
|
14181
|
-
console.error(" 1. In validate.qa UI: Project Settings \u2192 Local Runner \u2192 Generate Token (urt_...)");
|
|
14182
|
-
console.error(" 2. npx @validate.qa/runner login --token <urt_...> --server <the-server-url-printed-above-or-from-dashboard>");
|
|
14183
|
-
console.error("");
|
|
14184
|
-
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());
|
|
14185
15093
|
process.exit(EXIT_TERMINAL);
|
|
14186
15094
|
}
|
|
14187
15095
|
if (!serverUrl) {
|
|
@@ -14189,12 +15097,19 @@ program.command("start").description("Start the local test runner").option("--to
|
|
|
14189
15097
|
console.error(" Or pass --server <url> to this command");
|
|
14190
15098
|
process.exit(EXIT_TERMINAL);
|
|
14191
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
|
+
}
|
|
14192
15107
|
const isPersonalToken = authToken.startsWith("urt_");
|
|
14193
15108
|
if (!projectId && !isPersonalToken) {
|
|
14194
15109
|
console.error("\u2717 No project ID. Pass --project <id> or set PROJECT_ID env var");
|
|
14195
15110
|
process.exit(EXIT_TERMINAL);
|
|
14196
15111
|
}
|
|
14197
|
-
if (
|
|
15112
|
+
if (sendsTokenInPlaintext(serverUrl)) {
|
|
14198
15113
|
console.warn("\u26A0 WARNING: Server URL uses HTTP, not HTTPS. Runner token will be sent in plaintext.");
|
|
14199
15114
|
console.warn(" Consider using an HTTPS URL to protect credentials in transit.");
|
|
14200
15115
|
}
|
|
@@ -14213,7 +15128,14 @@ program.command("start").description("Start the local test runner").option("--to
|
|
|
14213
15128
|
console.warn("\u26A0 --tunnel has no effect without --mobile (the mobile bridge it exposes only starts with --mobile). Ignoring --tunnel.");
|
|
14214
15129
|
}
|
|
14215
15130
|
{
|
|
14216
|
-
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");
|
|
14217
15139
|
if (process.stdout.isTTY && !await hasLocalGoogleLogin()) {
|
|
14218
15140
|
console.log(`Tip: for apps with "Continue with Google", sign in to Google once on this machine: ${GOOGLE_LOGIN_COMMAND}`);
|
|
14219
15141
|
}
|
|
@@ -14243,7 +15165,7 @@ async function askLine(question) {
|
|
|
14243
15165
|
}
|
|
14244
15166
|
}
|
|
14245
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) => {
|
|
14246
|
-
const { signInLocalGoogle, LOCAL_GOOGLE_WARNING, grantedSite, defaultStoreKind } = await import("./dist-
|
|
15168
|
+
const { signInLocalGoogle, LOCAL_GOOGLE_WARNING, grantedSite, defaultStoreKind } = await import("./dist-BXHLIG5H.mjs");
|
|
14247
15169
|
console.log(LOCAL_GOOGLE_WARNING);
|
|
14248
15170
|
if (!opts.yes) {
|
|
14249
15171
|
const answer = await askLine('Is this a dedicated test Google account? Type "yes" to continue: ');
|
|
@@ -14278,7 +15200,7 @@ program.command("google-login").description('Sign in to a TEST Google account on
|
|
|
14278
15200
|
}
|
|
14279
15201
|
});
|
|
14280
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) => {
|
|
14281
|
-
const { allowLocalGoogleSites, grantedSite } = await import("./dist-
|
|
15203
|
+
const { allowLocalGoogleSites, grantedSite } = await import("./dist-BXHLIG5H.mjs");
|
|
14282
15204
|
const site = grantedSite(url);
|
|
14283
15205
|
if (!site) {
|
|
14284
15206
|
console.error("\u2717 Not a web address: give it like https://app.example.com");
|
|
@@ -14292,17 +15214,26 @@ program.command("google-allow-site <url>").description("Let the Google sign-in o
|
|
|
14292
15214
|
console.log(`\u2713 Tests on this machine may now use the Google sign-in for: ${sites.join(", ")}`);
|
|
14293
15215
|
});
|
|
14294
15216
|
program.command("google-logout").description("Forget the Google sign-in saved on this machine").action(async () => {
|
|
14295
|
-
const { deleteLocalGoogleLogin } = await import("./dist-
|
|
15217
|
+
const { deleteLocalGoogleLogin } = await import("./dist-BXHLIG5H.mjs");
|
|
14296
15218
|
await deleteLocalGoogleLogin();
|
|
14297
15219
|
console.log("\u2713 Google sign-in removed from this machine");
|
|
14298
15220
|
});
|
|
14299
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) => {
|
|
14300
15222
|
const saved = loadCredentials();
|
|
14301
|
-
const token = opts
|
|
14302
|
-
const serverUrl = (opts.server || process.env.SERVER_URL || saved?.serverUrl)?.replace(/\/+$/, "");
|
|
14303
|
-
const projectId = opts.project || process.env.PROJECT_ID || saved?.projectId;
|
|
15223
|
+
const { authToken: token, serverUrl, projectId, savedLoginSkipped } = resolveCredentials(opts, process.env, saved);
|
|
14304
15224
|
if (!token || !serverUrl) {
|
|
14305
|
-
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}`);
|
|
14306
15237
|
process.exit(1);
|
|
14307
15238
|
}
|
|
14308
15239
|
let appUrl;
|
|
@@ -14310,7 +15241,7 @@ program.command("sign-in").description("Sign in to your app once in a browser wi
|
|
|
14310
15241
|
try {
|
|
14311
15242
|
appUrl = new URL(opts.url);
|
|
14312
15243
|
console.log(`Opening ${appUrl.origin} in a browser window.`);
|
|
14313
|
-
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.");
|
|
14314
15245
|
capture = await captureSignInInteractively(appUrl.toString(), async (signal) => {
|
|
14315
15246
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
14316
15247
|
try {
|
|
@@ -14321,6 +15252,9 @@ program.command("sign-in").description("Sign in to your app once in a browser wi
|
|
|
14321
15252
|
} finally {
|
|
14322
15253
|
rl.close();
|
|
14323
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")
|
|
14324
15258
|
});
|
|
14325
15259
|
} catch (error) {
|
|
14326
15260
|
const message = describeSignInFailure(error, opts.url);
|
|
@@ -14333,43 +15267,23 @@ program.command("sign-in").description("Sign in to your app once in a browser wi
|
|
|
14333
15267
|
token,
|
|
14334
15268
|
projectId,
|
|
14335
15269
|
label: `Signed in on ${appUrl.hostname}`,
|
|
14336
|
-
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
|
+
} : {}
|
|
14337
15275
|
});
|
|
14338
|
-
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}`);
|
|
14339
15277
|
process.exit(result.ok ? 0 : 1);
|
|
14340
15278
|
});
|
|
14341
|
-
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) => {
|
|
14342
15280
|
await requireLatestRunner();
|
|
14343
|
-
|
|
14344
|
-
if (!saved) {
|
|
14345
|
-
console.log("No saved credentials. Run: validate-runner login");
|
|
14346
|
-
return;
|
|
14347
|
-
}
|
|
14348
|
-
console.log(`Server: ${saved.serverUrl}`);
|
|
14349
|
-
console.log(`Project: ${saved.projectId || "(not set)"}`);
|
|
14350
|
-
console.log(`Token: ${saved.token.slice(0, 8)}...${saved.token.slice(-4)}`);
|
|
14351
|
-
console.log("");
|
|
14352
|
-
try {
|
|
14353
|
-
const res = await fetch(`${saved.serverUrl}/api/runner/health`, {
|
|
14354
|
-
headers: {
|
|
14355
|
-
Authorization: `Bearer ${saved.token}`,
|
|
14356
|
-
...saved.projectId ? { "X-Project-Id": saved.projectId } : {}
|
|
14357
|
-
}
|
|
14358
|
-
});
|
|
14359
|
-
if (res.ok) {
|
|
14360
|
-
const data = await res.json();
|
|
14361
|
-
console.log(`\u2713 Server reachable - runner status: ${data.status} (${data.runners} runner(s) online)`);
|
|
14362
|
-
} else {
|
|
14363
|
-
console.log(`\u26A0 Server returned ${res.status}`);
|
|
14364
|
-
}
|
|
14365
|
-
} catch {
|
|
14366
|
-
console.error("\u2717 Could not reach server");
|
|
14367
|
-
}
|
|
15281
|
+
process.exitCode = await runStatusCheck(resolveStatusCredentials(opts, process.env, loadCredentials()));
|
|
14368
15282
|
});
|
|
14369
|
-
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) => {
|
|
14370
15284
|
const report = runDoctor();
|
|
14371
|
-
printDoctorReport(report);
|
|
14372
|
-
process.exit(report.
|
|
15285
|
+
printDoctorReport(report, { mobile: opts.mobile === true });
|
|
15286
|
+
process.exit(doctorExitCode(report, { mobile: opts.mobile === true }));
|
|
14373
15287
|
});
|
|
14374
15288
|
program.command("setup-mobile").description("Install Appium and the iOS (XCUITest) + Android (UiAutomator2) drivers").action(async () => {
|
|
14375
15289
|
const { spawn: spawn2 } = await import("child_process");
|