@validate.qa/runner 1.0.100 → 1.0.102
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 +3 -0
- package/dist/{chunk-MBKGTELT.mjs → chunk-3JM6XTSZ.mjs} +1862 -530
- package/dist/{chunk-2XD63NU5.mjs → chunk-4WLP3RWN.mjs} +941 -86
- package/dist/{chunk-MHFP3QXU.mjs → chunk-5DFBEL72.mjs} +1 -1
- package/dist/{chunk-II7QW7MZ.mjs → chunk-BXLEPT4M.mjs} +31 -18
- package/dist/{chunk-MAO6ADAO.mjs → chunk-CBVHPYVJ.mjs} +62 -2
- package/dist/{chunk-5P4ANVDY.mjs → chunk-DN3O6EYT.mjs} +43073 -23357
- package/dist/{chunk-GIIIVO2W.mjs → chunk-E2RRAE27.mjs} +155 -11
- package/dist/chunk-E2VEMBQQ.mjs +291 -0
- package/dist/{chunk-HMUC3DKS.mjs → chunk-HJ6FKHMM.mjs} +2281 -2
- package/dist/{chunk-WDXLAZ2J.mjs → chunk-PWFWMVZU.mjs} +3 -2
- package/dist/{chunk-6NIT2CCH.mjs → chunk-ZZYBRIBT.mjs} +18 -12
- package/dist/cli.js +139723 -114754
- package/dist/cli.mjs +536 -247
- package/dist/credential-mcp-server.js +1761 -358
- package/dist/credential-mcp-server.mjs +3 -3
- package/dist/discovery-mcp-server.js +1874 -379
- package/dist/discovery-mcp-server.mjs +4 -4
- package/dist/dist-52M5RDZ6.mjs +17 -0
- package/dist/inbox-mcp-server.js +1902 -505
- package/dist/inbox-mcp-server.mjs +3 -3
- package/dist/route-seed-worker.js +3976 -356
- package/dist/route-seed-worker.mjs +14 -7
- package/dist/run-write-guard-preload.js +2908 -572
- package/dist/run-write-guard-preload.mjs +5 -285
- package/dist/run-write-guard.js +2908 -572
- package/dist/run-write-guard.mjs +3 -3
- package/dist/spec-driver.js +1 -1
- package/dist/spec-driver.mjs +1 -1
- package/package.json +4 -3
- package/dist/dist-DFNLMH6L.mjs +0 -16
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
init_public_address,
|
|
4
|
+
public_address_exports,
|
|
3
5
|
require_ai_queue,
|
|
4
6
|
require_api_auth_headers,
|
|
5
7
|
require_api_call_redaction,
|
|
@@ -12,6 +14,7 @@ import {
|
|
|
12
14
|
require_failure_triage,
|
|
13
15
|
require_fetch_cli_subscription,
|
|
14
16
|
require_framework_import_runner,
|
|
17
|
+
require_google_local_signin,
|
|
15
18
|
require_grok_repo_enrichment_runner,
|
|
16
19
|
require_inbox_stamp,
|
|
17
20
|
require_live_browser_registry,
|
|
@@ -30,21 +33,23 @@ import {
|
|
|
30
33
|
require_resource_governor,
|
|
31
34
|
require_result_api_call_bodies,
|
|
32
35
|
require_result_shed
|
|
33
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-DN3O6EYT.mjs";
|
|
34
37
|
import {
|
|
35
38
|
require_tar_isolated
|
|
36
39
|
} from "./chunk-AAUHXGDS.mjs";
|
|
37
40
|
import "./chunk-EXRFHATN.mjs";
|
|
38
41
|
import {
|
|
39
|
-
require_browser_config
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
import "./chunk-
|
|
43
|
-
import "./chunk-
|
|
44
|
-
import "./chunk-
|
|
45
|
-
import "./chunk-
|
|
46
|
-
import "./chunk-
|
|
47
|
-
import "./chunk-
|
|
42
|
+
require_browser_config,
|
|
43
|
+
require_cloud_runner
|
|
44
|
+
} from "./chunk-ZZYBRIBT.mjs";
|
|
45
|
+
import "./chunk-BXLEPT4M.mjs";
|
|
46
|
+
import "./chunk-5DFBEL72.mjs";
|
|
47
|
+
import "./chunk-CBVHPYVJ.mjs";
|
|
48
|
+
import "./chunk-HJ6FKHMM.mjs";
|
|
49
|
+
import "./chunk-PWFWMVZU.mjs";
|
|
50
|
+
import "./chunk-E2VEMBQQ.mjs";
|
|
51
|
+
import "./chunk-4WLP3RWN.mjs";
|
|
52
|
+
import "./chunk-E2RRAE27.mjs";
|
|
48
53
|
import {
|
|
49
54
|
LIVE_FRAME_IDLE_PUSH_INTERVAL_MS,
|
|
50
55
|
MOBILE_COLD_BOOT_SETTLE_MS,
|
|
@@ -57,17 +62,12 @@ import {
|
|
|
57
62
|
buildAppiumCode,
|
|
58
63
|
clampMobileSwipeDistance,
|
|
59
64
|
classifyErrorMessage,
|
|
60
|
-
coldSessionConfigMessage,
|
|
61
65
|
computeMobileSwipePointers,
|
|
62
66
|
decideHarPosted,
|
|
63
67
|
deriveAuthPlan,
|
|
64
|
-
detectApiColdSessionBlock,
|
|
65
|
-
detectAuthBlock,
|
|
66
|
-
detectSkippedApiCoverage,
|
|
67
68
|
hasPlatformLocator,
|
|
68
69
|
init_dist,
|
|
69
70
|
isAppOriginResolutionEnabled,
|
|
70
|
-
isAuthReadinessGuardEnabled,
|
|
71
71
|
isConfigLikeReason,
|
|
72
72
|
isEditableMobileClassName,
|
|
73
73
|
isRepositoryCli,
|
|
@@ -84,18 +84,175 @@ import {
|
|
|
84
84
|
stepAppliesToPlatform,
|
|
85
85
|
stepsMissingPlatformLocators,
|
|
86
86
|
stepsNeedingReviewForPlatform
|
|
87
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-3JM6XTSZ.mjs";
|
|
88
88
|
import {
|
|
89
89
|
__commonJS,
|
|
90
|
+
__require,
|
|
91
|
+
__toCommonJS,
|
|
90
92
|
__toESM
|
|
91
93
|
} from "./chunk-WQXBGQU4.mjs";
|
|
92
94
|
|
|
95
|
+
// ../runner-core/dist/utils/playwright-signal-handlers.js
|
|
96
|
+
var require_playwright_signal_handlers = __commonJS({
|
|
97
|
+
"../runner-core/dist/utils/playwright-signal-handlers.js"(exports) {
|
|
98
|
+
"use strict";
|
|
99
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
100
|
+
exports.patchBrowserTypeSignalDefaults = patchBrowserTypeSignalDefaults;
|
|
101
|
+
exports.disablePlaywrightSignalHandlers = disablePlaywrightSignalHandlers2;
|
|
102
|
+
var playwright_1 = __require("playwright");
|
|
103
|
+
var PATCHED = /* @__PURE__ */ Symbol.for("validateqa.playwrightSignalDefaults");
|
|
104
|
+
var LAUNCH_METHODS = { launch: 0, launchPersistentContext: 1 };
|
|
105
|
+
function patchBrowserTypeSignalDefaults(browserType) {
|
|
106
|
+
const target = browserType;
|
|
107
|
+
if (target[PATCHED])
|
|
108
|
+
return;
|
|
109
|
+
for (const [method, optionsIndex] of Object.entries(LAUNCH_METHODS)) {
|
|
110
|
+
const original = target[method];
|
|
111
|
+
if (typeof original !== "function")
|
|
112
|
+
continue;
|
|
113
|
+
target[method] = function(...args) {
|
|
114
|
+
const given = args[optionsIndex];
|
|
115
|
+
args[optionsIndex] = { handleSIGINT: false, handleSIGTERM: false, ...given && typeof given === "object" ? given : {} };
|
|
116
|
+
return original.apply(this, args);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
target[PATCHED] = true;
|
|
120
|
+
}
|
|
121
|
+
function disablePlaywrightSignalHandlers2() {
|
|
122
|
+
patchBrowserTypeSignalDefaults(Object.getPrototypeOf(playwright_1.chromium));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ../runner-core/dist/utils/tls-cert-probe.js
|
|
128
|
+
var require_tls_cert_probe = __commonJS({
|
|
129
|
+
"../runner-core/dist/utils/tls-cert-probe.js"(exports) {
|
|
130
|
+
"use strict";
|
|
131
|
+
var __importDefault = exports && exports.__importDefault || function(mod) {
|
|
132
|
+
return mod && mod.__esModule ? mod : { "default": mod };
|
|
133
|
+
};
|
|
134
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
135
|
+
exports.RUN_NOTE_WAIT_MS = void 0;
|
|
136
|
+
exports.probeTargetCertificate = probeTargetCertificate;
|
|
137
|
+
exports.tlsCertFindingLine = tlsCertFindingLine;
|
|
138
|
+
exports.targetCertificateNote = targetCertificateNote2;
|
|
139
|
+
exports.noteWithinCap = noteWithinCap2;
|
|
140
|
+
exports.withRunNote = withRunNote2;
|
|
141
|
+
var node_dns_1 = __require("dns");
|
|
142
|
+
var node_net_1 = __importDefault(__require("net"));
|
|
143
|
+
var node_tls_1 = __importDefault(__require("tls"));
|
|
144
|
+
var public_address_1 = (init_public_address(), __toCommonJS(public_address_exports));
|
|
145
|
+
var cloud_runner_js_1 = require_cloud_runner();
|
|
146
|
+
var PROBE_TIMEOUT_MS = 5e3;
|
|
147
|
+
var CERT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
148
|
+
"CERT_HAS_EXPIRED",
|
|
149
|
+
"CERT_NOT_YET_VALID",
|
|
150
|
+
"CERT_REVOKED",
|
|
151
|
+
"CERT_REJECTED",
|
|
152
|
+
"CERT_UNTRUSTED",
|
|
153
|
+
"CERT_SIGNATURE_FAILURE",
|
|
154
|
+
"CERT_CHAIN_TOO_LONG",
|
|
155
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
156
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
157
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
158
|
+
"UNABLE_TO_GET_ISSUER_CERT",
|
|
159
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
160
|
+
"UNABLE_TO_DECRYPT_CERT_SIGNATURE",
|
|
161
|
+
"UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY",
|
|
162
|
+
"ERROR_IN_CERT_NOT_BEFORE_FIELD",
|
|
163
|
+
"ERROR_IN_CERT_NOT_AFTER_FIELD",
|
|
164
|
+
"INVALID_CA",
|
|
165
|
+
"INVALID_PURPOSE",
|
|
166
|
+
"PATH_LENGTH_EXCEEDED",
|
|
167
|
+
"HOSTNAME_MISMATCH",
|
|
168
|
+
"ERR_TLS_CERT_ALTNAME_INVALID"
|
|
169
|
+
]);
|
|
170
|
+
async function probeTargetCertificate(baseUrl, opts = {}) {
|
|
171
|
+
let url;
|
|
172
|
+
try {
|
|
173
|
+
url = new URL(baseUrl);
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (url.protocol !== "https:" || !url.hostname)
|
|
178
|
+
return null;
|
|
179
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
180
|
+
const port = url.port ? Number(url.port) : 443;
|
|
181
|
+
const shared = opts.shared ?? (0, cloud_runner_js_1.isSharedCloudRunner)();
|
|
182
|
+
const ipLiteral = node_net_1.default.isIP(hostname) !== 0;
|
|
183
|
+
if (shared && ipLiteral && (0, public_address_1.isPrivateOrLocalIpAddress)(hostname))
|
|
184
|
+
return null;
|
|
185
|
+
return new Promise((resolve3) => {
|
|
186
|
+
let settled = false;
|
|
187
|
+
let socket = null;
|
|
188
|
+
const finish = (finding) => {
|
|
189
|
+
if (settled)
|
|
190
|
+
return;
|
|
191
|
+
settled = true;
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
socket?.destroy();
|
|
194
|
+
resolve3(finding);
|
|
195
|
+
};
|
|
196
|
+
const timer = setTimeout(() => finish(null), opts.timeoutMs ?? PROBE_TIMEOUT_MS);
|
|
197
|
+
try {
|
|
198
|
+
socket = node_tls_1.default.connect({
|
|
199
|
+
host: hostname,
|
|
200
|
+
port,
|
|
201
|
+
...ipLiteral ? {} : { servername: hostname },
|
|
202
|
+
rejectUnauthorized: true,
|
|
203
|
+
...opts.ca ? { ca: opts.ca } : {},
|
|
204
|
+
...shared ? { lookup: (0, public_address_1.createPublicOnlyLookup)((name, o, cb) => (0, node_dns_1.lookup)(name, o, cb)) } : {},
|
|
205
|
+
ALPNProtocols: ["http/1.1"]
|
|
206
|
+
});
|
|
207
|
+
} catch {
|
|
208
|
+
finish(null);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
socket.once("secureConnect", () => finish(null));
|
|
212
|
+
socket.once("error", (err) => {
|
|
213
|
+
const code = typeof err.code === "string" ? err.code : "";
|
|
214
|
+
finish(CERT_ERROR_CODES.has(code) ? { host: url.host, code, detail: err.message.replace(/\s+/g, " ").trim().slice(0, 300) } : null);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function tlsCertFindingLine(finding) {
|
|
219
|
+
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
|
+
}
|
|
221
|
+
async function targetCertificateNote2(baseUrl, opts = {}) {
|
|
222
|
+
try {
|
|
223
|
+
const finding = await probeTargetCertificate(baseUrl, opts);
|
|
224
|
+
return finding ? tlsCertFindingLine(finding) : null;
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
exports.RUN_NOTE_WAIT_MS = 250;
|
|
230
|
+
async function noteWithinCap2(note, capMs = exports.RUN_NOTE_WAIT_MS) {
|
|
231
|
+
let timer;
|
|
232
|
+
const cap = new Promise((resolve3) => {
|
|
233
|
+
timer = setTimeout(() => resolve3(null), capMs);
|
|
234
|
+
});
|
|
235
|
+
try {
|
|
236
|
+
return await Promise.race([note.catch(() => null), cap]);
|
|
237
|
+
} finally {
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function withRunNote2(result, note) {
|
|
242
|
+
if (!note)
|
|
243
|
+
return result;
|
|
244
|
+
return { ...result, logs: result.logs ? `${note}
|
|
245
|
+
${result.logs}` : note };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
93
250
|
// package.json
|
|
94
251
|
var require_package = __commonJS({
|
|
95
252
|
"package.json"(exports, module) {
|
|
96
253
|
module.exports = {
|
|
97
254
|
name: "@validate.qa/runner",
|
|
98
|
-
version: "1.0.
|
|
255
|
+
version: "1.0.102",
|
|
99
256
|
description: "validate local test runner - execute Playwright tests on your machine, connected to validate.qa cloud.",
|
|
100
257
|
bin: {
|
|
101
258
|
"validate-runner": "dist/cli.js"
|
|
@@ -103,8 +260,9 @@ var require_package = __commonJS({
|
|
|
103
260
|
scripts: {
|
|
104
261
|
build: "tsup",
|
|
105
262
|
dev: "tsx src/cli.ts",
|
|
106
|
-
|
|
107
|
-
|
|
263
|
+
lint: "eslint .",
|
|
264
|
+
test: "node --import tsx --test src/supervisor-child-argv.test.ts src/audit-scrub.test.ts src/retry-on-429.test.ts src/version-check.test.ts src/poll-interval.test.ts src/node-version-notice.test.ts src/services/auth-state.test.ts src/run-no-longer-active.test.ts src/mode-abort-no-verdict.test.ts src/discovery-render-type.test.ts src/batch-heal-no-start.test.ts src/heartbeat-active-runs.test.ts src/result-post-structure-413.test.ts src/result-shed-routes-413.test.ts src/result-post-retry.test.ts src/result-media-stale-epoch.test.ts src/run-write-guard-bundle.test.ts src/spec-driver-bundle.test.ts src/mcp-servers-bundle.test.ts src/codex-files-mcp-server-bundle.test.ts src/claim-nonce-not-logged.test.ts src/sign-in-capture.test.ts src/run-status-verdicts.test.ts src/sign-in-failure.test.ts src/mobile-ui-path-succeeded.test.ts src/supervisor-policy.test.ts src/login-check.test.ts src/heal-post-lost-ack.test.ts src/ui-heal-result-video.test.ts src/heal-log-stream-chunks.test.ts src/shutdown-claim-window.test.ts src/result-post-redeploy-budget.test.ts src/shutdown-signals.test.ts",
|
|
265
|
+
"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",
|
|
108
266
|
"smoke:installed": "node scripts/installed-run-smoke.mjs",
|
|
109
267
|
"check:packed-mcp": "node scripts/packed-install-check.mjs",
|
|
110
268
|
prepublishOnly: "npm run build"
|
|
@@ -315,17 +473,19 @@ var import_playwright_native = __toESM(require_playwright_native());
|
|
|
315
473
|
var import_runner_core = __toESM(require_dist());
|
|
316
474
|
|
|
317
475
|
// src/runner.ts
|
|
318
|
-
var import_runner_core6 = __toESM(require_dist());
|
|
319
476
|
var import_runner_core7 = __toESM(require_dist());
|
|
477
|
+
var import_runner_core8 = __toESM(require_dist());
|
|
478
|
+
var import_runner_core9 = __toESM(require_dist());
|
|
320
479
|
var import_framework_import_runner = __toESM(require_framework_import_runner());
|
|
321
480
|
var import_grok_repo_enrichment_runner = __toESM(require_grok_repo_enrichment_runner());
|
|
322
481
|
var import_repo_brief_execution = __toESM(require_repo_brief_execution());
|
|
323
482
|
var import_repo_read_log = __toESM(require_repo_read_log());
|
|
324
483
|
var import_cli_session_failure = __toESM(require_cli_session_failure());
|
|
484
|
+
var import_playwright_signal_handlers = __toESM(require_playwright_signal_handlers());
|
|
325
485
|
var import_cli_limit = __toESM(require_cli_limit());
|
|
326
486
|
var import_fetch_cli_subscription = __toESM(require_fetch_cli_subscription());
|
|
327
487
|
var import_codex_cli = __toESM(require_codex_cli());
|
|
328
|
-
var
|
|
488
|
+
var import_runner_core10 = __toESM(require_dist());
|
|
329
489
|
|
|
330
490
|
// src/services/regression-runner.ts
|
|
331
491
|
var import_regression_runner = __toESM(require_regression_runner());
|
|
@@ -383,6 +543,7 @@ var EXIT_TERMINAL = 64;
|
|
|
383
543
|
var SUPERVISOR_WINDOW_MS = 36e5;
|
|
384
544
|
var SUPERVISOR_HEALTHY_MS = 10 * 6e4;
|
|
385
545
|
function nextSupervisorAction(input, state2, limits) {
|
|
546
|
+
if (input.signalled) return { action: "stop", exitCode: 0, reason: "clean" };
|
|
386
547
|
if (input.code === 0) return { action: "stop", exitCode: 0, reason: "clean" };
|
|
387
548
|
if (input.code === EXIT_TERMINAL) return { action: "stop", exitCode: EXIT_TERMINAL, reason: "terminal" };
|
|
388
549
|
const restartTimes = [...state2.restartTimes.filter((t) => input.now - t < SUPERVISOR_WINDOW_MS), input.now];
|
|
@@ -534,6 +695,24 @@ function upgradeAction(activeRuns) {
|
|
|
534
695
|
return activeRuns > 0 ? "drain" : "exit";
|
|
535
696
|
}
|
|
536
697
|
|
|
698
|
+
// src/shutdown-signals.ts
|
|
699
|
+
var REPEAT_SIGNAL_WINDOW_MS = 1e3;
|
|
700
|
+
function installShutdownSignals(target, onShutdown, opts) {
|
|
701
|
+
const now = opts.now ?? Date.now;
|
|
702
|
+
const windowMs = opts.repeatWindowMs ?? REPEAT_SIGNAL_WINDOW_MS;
|
|
703
|
+
let firstAt = null;
|
|
704
|
+
const handle = () => {
|
|
705
|
+
if (firstAt === null) {
|
|
706
|
+
firstAt = now();
|
|
707
|
+
onShutdown();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (now() - firstAt > windowMs) opts.forceExit();
|
|
711
|
+
};
|
|
712
|
+
target.on("SIGINT", handle);
|
|
713
|
+
target.on("SIGTERM", handle);
|
|
714
|
+
}
|
|
715
|
+
|
|
537
716
|
// src/runner.ts
|
|
538
717
|
init_dist();
|
|
539
718
|
|
|
@@ -680,6 +859,9 @@ function computeMobileUiPathSucceeded(stepResults, authoredSteps) {
|
|
|
680
859
|
return passedBefore > 0;
|
|
681
860
|
}
|
|
682
861
|
|
|
862
|
+
// src/post-heal-outcome.ts
|
|
863
|
+
var import_runner_core5 = __toESM(require_dist());
|
|
864
|
+
|
|
683
865
|
// src/run-no-longer-active.ts
|
|
684
866
|
var RunNoLongerActiveError = class extends Error {
|
|
685
867
|
};
|
|
@@ -691,39 +873,19 @@ function withClaimEpoch(body, claimEpoch) {
|
|
|
691
873
|
}
|
|
692
874
|
|
|
693
875
|
// src/post-heal-outcome.ts
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const tryPostPatch = async () => {
|
|
697
|
-
if (passed && healedCode && testCaseId) {
|
|
698
|
-
try {
|
|
699
|
-
await postHealResult2(healedCode, testCaseId);
|
|
700
|
-
log2?.ok?.(`\u{1FA7A} Healed code auto-patched for test ${testCaseId}`);
|
|
701
|
-
} catch (patchErr) {
|
|
702
|
-
const patchMsg = patchErr instanceof Error ? patchErr.message : String(patchErr);
|
|
703
|
-
log2?.warn?.(`Failed to auto-patch healed code: ${patchMsg}`);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
};
|
|
707
|
-
try {
|
|
708
|
-
await postRunResult();
|
|
709
|
-
} catch (err) {
|
|
710
|
-
if (err instanceof RunNoLongerActiveError) {
|
|
711
|
-
await tryPostPatch();
|
|
712
|
-
}
|
|
713
|
-
throw err;
|
|
714
|
-
}
|
|
715
|
-
await tryPostPatch();
|
|
876
|
+
function postHealOutcome(options) {
|
|
877
|
+
return (0, import_runner_core5.postHealOutcome)({ ...options, isRunGone: (err) => err instanceof RunNoLongerActiveError });
|
|
716
878
|
}
|
|
717
879
|
|
|
718
880
|
// src/run-status-verdicts.ts
|
|
719
|
-
var
|
|
881
|
+
var import_runner_core6 = __toESM(require_dist());
|
|
720
882
|
function runStatusVerdicts(body, heldEpoch) {
|
|
721
883
|
const rows = body?.runs;
|
|
722
884
|
if (!Array.isArray(rows)) return [];
|
|
723
885
|
const verdicts = [];
|
|
724
886
|
for (const row of rows) {
|
|
725
887
|
if (!row || typeof row.id !== "string" || typeof row.status !== "string") continue;
|
|
726
|
-
const reason = (0,
|
|
888
|
+
const reason = (0, import_runner_core6.runStatusAbortReason)(row, heldEpoch(row.id));
|
|
727
889
|
if (!reason) continue;
|
|
728
890
|
verdicts.push({
|
|
729
891
|
runId: row.id,
|
|
@@ -1646,7 +1808,8 @@ async function clearField(sessionPath, bounds) {
|
|
|
1646
1808
|
} catch (err) {
|
|
1647
1809
|
if (isAppiumDriverDeadError(err)) throw err;
|
|
1648
1810
|
throw new Error(
|
|
1649
|
-
`No focused element available for CLEAR. Tap an input field first.${err instanceof Error ? ` (${err.message})` : ""}
|
|
1811
|
+
`No focused element available for CLEAR. Tap an input field first.${err instanceof Error ? ` (${err.message})` : ""}`,
|
|
1812
|
+
{ cause: err }
|
|
1650
1813
|
);
|
|
1651
1814
|
}
|
|
1652
1815
|
}
|
|
@@ -3876,7 +4039,7 @@ async function getWindowSize2(sessionId, retries = 3) {
|
|
|
3876
4039
|
} catch (err) {
|
|
3877
4040
|
if (attempt === retries - 1) {
|
|
3878
4041
|
if (isTransportError(err)) throw err;
|
|
3879
|
-
throw new Error(`Failed to get window size after ${retries} attempts: ${err instanceof Error ? err.message : String(err)}
|
|
4042
|
+
throw new Error(`Failed to get window size after ${retries} attempts: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
3880
4043
|
}
|
|
3881
4044
|
await new Promise((resolve3) => setTimeout(resolve3, Math.pow(2, attempt) * 100));
|
|
3882
4045
|
}
|
|
@@ -5243,7 +5406,7 @@ async function executeSwipe(sessionId, step) {
|
|
|
5243
5406
|
screenH = windowSize.height;
|
|
5244
5407
|
} catch (err) {
|
|
5245
5408
|
if (isTransportError(err)) throw err;
|
|
5246
|
-
throw new Error(`Swipe action failed: could not determine screen dimensions. ${err instanceof Error ? err.message : String(err)}
|
|
5409
|
+
throw new Error(`Swipe action failed: could not determine screen dimensions. ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
5247
5410
|
}
|
|
5248
5411
|
const { startX, startY, endX, endY } = computeMobileSwipePointers(screenW, screenH, direction, distance);
|
|
5249
5412
|
await wdRequest(`/session/${sessionId}/actions`, {
|
|
@@ -5562,12 +5725,12 @@ async function executeMobileTest(options) {
|
|
|
5562
5725
|
const base = err instanceof Error ? err.message : String(err);
|
|
5563
5726
|
if (selectedDevice.isPhysical) {
|
|
5564
5727
|
const guidance = platformFilter === "IOS" ? `Real iOS devices require a signed WebDriverAgent (configure signing in Xcode, or pass appium:xcodeOrgId / appium:xcodeSigningId) and the app (${options.target.appId}) must be installed and trusted on the device.` : `Ensure ${selectedDevice.name} is authorized for USB debugging and ${options.target.appId} is installed.`;
|
|
5565
|
-
throw new Error(`${base} \u2014 could not start an Appium session on the physical device ${selectedDevice.name}. ${guidance}
|
|
5728
|
+
throw new Error(`${base} \u2014 could not start an Appium session on the physical device ${selectedDevice.name}. ${guidance}`, { cause: err });
|
|
5566
5729
|
}
|
|
5567
5730
|
const appListUnknown = !selectedDevice.installedApps || selectedDevice.installedApps.length === 0;
|
|
5568
5731
|
if (appListUnknown) {
|
|
5569
5732
|
const installHint = platformFilter === "IOS" ? `xcrun simctl install ${selectedDevice.id} <app-path>` : `adb -s ${selectedDevice.id} install <apk-path>`;
|
|
5570
|
-
throw new Error(`${base} \u2014 could not confirm ${options.target.appId} is installed on ${selectedDevice.name}; if it is missing, install it first: ${installHint}
|
|
5733
|
+
throw new Error(`${base} \u2014 could not confirm ${options.target.appId} is installed on ${selectedDevice.name}; if it is missing, install it first: ${installHint}`, { cause: err });
|
|
5571
5734
|
}
|
|
5572
5735
|
throw err;
|
|
5573
5736
|
}
|
|
@@ -5607,7 +5770,7 @@ async function executeMobileTest(options) {
|
|
|
5607
5770
|
detail,
|
|
5608
5771
|
recordedFromClearedState
|
|
5609
5772
|
);
|
|
5610
|
-
if (requirementError) throw new Error(requirementError);
|
|
5773
|
+
if (requirementError) throw new Error(requirementError, { cause: err });
|
|
5611
5774
|
log2(`App-data clear failed (AUTO continues with plain fresh launch): ${detail}`);
|
|
5612
5775
|
}
|
|
5613
5776
|
} else {
|
|
@@ -5642,7 +5805,8 @@ async function executeMobileTest(options) {
|
|
|
5642
5805
|
await openDeepLink(sessionId, options.target.platform, options.target.deeplink, options.target.appId);
|
|
5643
5806
|
} catch (err) {
|
|
5644
5807
|
throw new Error(
|
|
5645
|
-
`Deep-link launch failed; refusing to replay from the wrong entry screen: ${err instanceof Error ? err.message : String(err)}
|
|
5808
|
+
`Deep-link launch failed; refusing to replay from the wrong entry screen: ${err instanceof Error ? err.message : String(err)}`,
|
|
5809
|
+
{ cause: err }
|
|
5646
5810
|
);
|
|
5647
5811
|
}
|
|
5648
5812
|
}
|
|
@@ -5923,7 +6087,8 @@ async function createMobileDiscoverySession(target, options) {
|
|
|
5923
6087
|
});
|
|
5924
6088
|
releaseControlPortSlot(discoveryPortSlotDeviceId);
|
|
5925
6089
|
throw new Error(
|
|
5926
|
-
`Deep-link launch failed for mobile discovery: ${err instanceof Error ? err.message : String(err)}
|
|
6090
|
+
`Deep-link launch failed for mobile discovery: ${err instanceof Error ? err.message : String(err)}`,
|
|
6091
|
+
{ cause: err }
|
|
5927
6092
|
);
|
|
5928
6093
|
}
|
|
5929
6094
|
}
|
|
@@ -6979,7 +7144,8 @@ async function startSession(validated, res) {
|
|
|
6979
7144
|
} catch (err) {
|
|
6980
7145
|
const detail = err instanceof Error ? err.message : String(err);
|
|
6981
7146
|
throw new Error(
|
|
6982
|
-
`Failed to open the configured deep link before recording (${detail}). The Appium session was closed; verify the URL and app deep-link configuration, then try again
|
|
7147
|
+
`Failed to open the configured deep link before recording (${detail}). The Appium session was closed; verify the URL and app deep-link configuration, then try again.`,
|
|
7148
|
+
{ cause: err }
|
|
6983
7149
|
);
|
|
6984
7150
|
}
|
|
6985
7151
|
}
|
|
@@ -8907,23 +9073,42 @@ function withMediaReferences(result, refs) {
|
|
|
8907
9073
|
// src/result-post-retry.ts
|
|
8908
9074
|
var RESULT_POST_MAX_ATTEMPTS = 3;
|
|
8909
9075
|
var RESULT_POST_BACKOFF_BASE_MS = 1e3;
|
|
9076
|
+
var RESULT_POST_BACKOFF_CAP_MS = 15e3;
|
|
9077
|
+
var RESULT_POST_RETRY_BUDGET_DEFAULT_MS = 15e4;
|
|
9078
|
+
function resultPostRetryBudgetMs(env = process.env) {
|
|
9079
|
+
return Math.max(1e4, Number.parseInt(env.RUNNER_RESULT_RETRY_BUDGET_MS ?? "", 10) || RESULT_POST_RETRY_BUDGET_DEFAULT_MS);
|
|
9080
|
+
}
|
|
9081
|
+
function defaultResultPostBudgetMs(backoffBaseMs) {
|
|
9082
|
+
return backoffBaseMs > 0 ? resultPostRetryBudgetMs() : 0;
|
|
9083
|
+
}
|
|
9084
|
+
function nextResultPostDelay(attempt, opts) {
|
|
9085
|
+
const delay = Math.min(RESULT_POST_BACKOFF_CAP_MS, Math.pow(2, attempt) * opts.backoffBaseMs);
|
|
9086
|
+
if (attempt < opts.maxAttempts) return delay;
|
|
9087
|
+
const left = opts.startedAt + opts.budgetMs - (opts.now ?? Date.now());
|
|
9088
|
+
if (left <= 0) return null;
|
|
9089
|
+
return Math.min(delay, left);
|
|
9090
|
+
}
|
|
8910
9091
|
function isTransientResultPostStatus(status) {
|
|
8911
9092
|
return status === 408 || status === 429 || status >= 500;
|
|
8912
9093
|
}
|
|
8913
|
-
async function postRetryingTransient(send, label, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS) {
|
|
9094
|
+
async function postRetryingTransient(send, label, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS, budgetMs = defaultResultPostBudgetMs(backoffBaseMs)) {
|
|
9095
|
+
const startedAt = Date.now();
|
|
8914
9096
|
for (let attempt = 1; ; attempt++) {
|
|
8915
9097
|
let failure;
|
|
9098
|
+
let delay;
|
|
8916
9099
|
try {
|
|
8917
9100
|
const res = await send();
|
|
8918
|
-
if (!isTransientResultPostStatus(res.status)
|
|
9101
|
+
if (!isTransientResultPostStatus(res.status)) return res;
|
|
9102
|
+
delay = nextResultPostDelay(attempt, { maxAttempts: RESULT_POST_MAX_ATTEMPTS, startedAt, budgetMs, backoffBaseMs });
|
|
9103
|
+
if (delay === null) return res;
|
|
8919
9104
|
await res.body?.cancel().catch(() => {
|
|
8920
9105
|
});
|
|
8921
9106
|
failure = `HTTP ${res.status}`;
|
|
8922
9107
|
} catch (err) {
|
|
8923
|
-
|
|
9108
|
+
delay = nextResultPostDelay(attempt, { maxAttempts: RESULT_POST_MAX_ATTEMPTS, startedAt, budgetMs, backoffBaseMs });
|
|
9109
|
+
if (delay === null) throw err;
|
|
8924
9110
|
failure = err instanceof Error ? err.message : String(err);
|
|
8925
9111
|
}
|
|
8926
|
-
const delay = Math.pow(2, attempt) * backoffBaseMs;
|
|
8927
9112
|
console.warn(`[runner] ${label} post attempt ${attempt} failed (${failure}), retrying in ${delay}ms...`);
|
|
8928
9113
|
await new Promise((resolve3) => setTimeout(resolve3, delay));
|
|
8929
9114
|
}
|
|
@@ -8956,6 +9141,7 @@ function clampPollInterval(value) {
|
|
|
8956
9141
|
init_dist();
|
|
8957
9142
|
var import_ai_queue = __toESM(require_ai_queue());
|
|
8958
9143
|
var import_base_url_check = __toESM(require_base_url_check());
|
|
9144
|
+
var import_tls_cert_probe = __toESM(require_tls_cert_probe());
|
|
8959
9145
|
var import_rate_limit_detector = __toESM(require_rate_limit_detector());
|
|
8960
9146
|
var import_resource_governor = __toESM(require_resource_governor());
|
|
8961
9147
|
var import_process_inventory = __toESM(require_process_inventory());
|
|
@@ -9170,7 +9356,7 @@ async function claimRun(serverUrl, headers, runId) {
|
|
|
9170
9356
|
if (typeof body.claimNonce === "string" && body.claimNonce) claimNonces.set(runId, body.claimNonce);
|
|
9171
9357
|
return body.claimEpoch;
|
|
9172
9358
|
}
|
|
9173
|
-
async function postResult(serverUrl, headers, runId, result, claimEpoch, maxAttemptsArg = 3, backoffBaseMs = 1e3) {
|
|
9359
|
+
async function postResult(serverUrl, headers, runId, result, claimEpoch, maxAttemptsArg = 3, backoffBaseMs = 1e3, budgetMs = defaultResultPostBudgetMs(backoffBaseMs)) {
|
|
9174
9360
|
let maxAttempts = maxAttemptsArg;
|
|
9175
9361
|
let verdictOnlyApplied = false;
|
|
9176
9362
|
let lastError;
|
|
@@ -9189,8 +9375,10 @@ async function postResult(serverUrl, headers, runId, result, claimEpoch, maxAtte
|
|
|
9189
9375
|
}
|
|
9190
9376
|
let payload = budgeted.payload;
|
|
9191
9377
|
let apiCallBodiesStripped = false;
|
|
9378
|
+
const startedAt = Date.now();
|
|
9192
9379
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
9193
9380
|
attemptsMade = attempt;
|
|
9381
|
+
let transient = false;
|
|
9194
9382
|
try {
|
|
9195
9383
|
const res = await fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/result`, () => ({
|
|
9196
9384
|
method: "POST",
|
|
@@ -9242,15 +9430,17 @@ async function postResult(serverUrl, headers, runId, result, claimEpoch, maxAtte
|
|
|
9242
9430
|
throw new RunNoLongerActiveError(`Run ${runId} is no longer active on the server (HTTP ${res.status})`);
|
|
9243
9431
|
}
|
|
9244
9432
|
lastError = new Error(`HTTP ${res.status}`);
|
|
9433
|
+
transient = isTransientResultPostStatus(res.status);
|
|
9245
9434
|
} catch (err) {
|
|
9246
9435
|
if (err instanceof RunNoLongerActiveError) throw err;
|
|
9247
9436
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
9437
|
+
transient = true;
|
|
9248
9438
|
}
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9439
|
+
const delay = nextResultPostDelay(attempt, { maxAttempts, startedAt, budgetMs: transient ? budgetMs : 0, backoffBaseMs });
|
|
9440
|
+
if (delay === null) break;
|
|
9441
|
+
if (attempt >= maxAttempts) maxAttempts++;
|
|
9442
|
+
console.warn(`[runner] postResult attempt ${attempt} failed (${lastError.message}), retrying in ${delay}ms...`);
|
|
9443
|
+
await new Promise((resolve3) => setTimeout(resolve3, delay));
|
|
9254
9444
|
}
|
|
9255
9445
|
throw new Error(`Failed to post result after ${attemptsMade} attempts: ${lastError?.message}`);
|
|
9256
9446
|
}
|
|
@@ -9280,7 +9470,7 @@ async function postHealResult(serverUrl, headers, runId, healedCode, testCaseId,
|
|
|
9280
9470
|
}
|
|
9281
9471
|
throw new Error(`Failed to post heal result after ${MAX_ATTEMPTS} attempts: ${lastError?.message}`);
|
|
9282
9472
|
}
|
|
9283
|
-
async function postMobileHealResult(serverUrl, headers, runId, body) {
|
|
9473
|
+
async function postMobileHealResult(serverUrl, headers, runId, body, claimEpoch) {
|
|
9284
9474
|
const MAX_ATTEMPTS = 3;
|
|
9285
9475
|
let lastError;
|
|
9286
9476
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
@@ -9288,7 +9478,7 @@ async function postMobileHealResult(serverUrl, headers, runId, body) {
|
|
|
9288
9478
|
const res = await fetch(`${serverUrl}/api/runner/runs/${runId}/mobile-heal-result`, {
|
|
9289
9479
|
method: "POST",
|
|
9290
9480
|
headers,
|
|
9291
|
-
body: JSON.stringify(body),
|
|
9481
|
+
body: JSON.stringify(withClaimEpoch(body, claimEpoch)),
|
|
9292
9482
|
signal: AbortSignal.timeout(6e4)
|
|
9293
9483
|
});
|
|
9294
9484
|
if (res.ok) return;
|
|
@@ -9307,12 +9497,13 @@ async function postMobileHealResult(serverUrl, headers, runId, body) {
|
|
|
9307
9497
|
function narrowMobileLaunchMode(mode) {
|
|
9308
9498
|
return mode === "LAUNCH_APP" || mode === "DEEP_LINK" || mode === "RESUME" ? mode : void 0;
|
|
9309
9499
|
}
|
|
9310
|
-
async function postBatchHealResult(serverUrl, headers, runId, body, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS) {
|
|
9500
|
+
async function postBatchHealResult(serverUrl, headers, runId, body, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS, claimEpoch) {
|
|
9311
9501
|
const progress = { body, fromRung: 0 };
|
|
9312
9502
|
const res = await postRetryingTransient(() => postShedOn413((payload) => fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/batch-heal-result`, () => ({
|
|
9313
9503
|
method: "POST",
|
|
9314
9504
|
headers,
|
|
9315
|
-
body:
|
|
9505
|
+
// At send time, not in the body: the verdict-only rung keeps a fixed list of fields and would drop it.
|
|
9506
|
+
body: JSON.stringify(withClaimEpoch(payload, claimEpoch)),
|
|
9316
9507
|
signal: AbortSignal.timeout(12e4)
|
|
9317
9508
|
})), progress.body, "batch heal result", import_result_shed.BATCH_HEAL_SHED_LADDER, progress), "batch heal result", backoffBaseMs);
|
|
9318
9509
|
throwUnlessResultLanded(res, runId, "batch heal result");
|
|
@@ -9324,11 +9515,11 @@ function throwUnlessResultLanded(res, runId, label) {
|
|
|
9324
9515
|
}
|
|
9325
9516
|
throw new Error(`Failed to post ${label}: ${res.status}`);
|
|
9326
9517
|
}
|
|
9327
|
-
async function postBatchHealPartial(serverUrl, headers, runId, result) {
|
|
9518
|
+
async function postBatchHealPartial(serverUrl, headers, runId, result, claimEpoch) {
|
|
9328
9519
|
const res = await postShedOn413((payload) => fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/batch-heal-partial`, () => ({
|
|
9329
9520
|
method: "POST",
|
|
9330
9521
|
headers,
|
|
9331
|
-
body: JSON.stringify(payload),
|
|
9522
|
+
body: JSON.stringify(withClaimEpoch(payload, claimEpoch)),
|
|
9332
9523
|
// Fires per verdict: a slow server must not hold up the next test.
|
|
9333
9524
|
signal: AbortSignal.timeout(3e4)
|
|
9334
9525
|
})), { result }, "batch heal partial", import_result_shed.BATCH_HEAL_PARTIAL_SHED_LADDER);
|
|
@@ -9349,11 +9540,11 @@ async function fetchBatchHealAppliedIds(serverUrl, headers, runId, projectId) {
|
|
|
9349
9540
|
return /* @__PURE__ */ new Set();
|
|
9350
9541
|
}
|
|
9351
9542
|
}
|
|
9352
|
-
function batchHealPartialHooks(serverUrl, headers, runId, projectId, warn = (msg) => log.warn(msg)) {
|
|
9543
|
+
function batchHealPartialHooks(serverUrl, headers, runId, projectId, warn = (msg) => log.warn(msg), claimEpoch) {
|
|
9353
9544
|
return {
|
|
9354
9545
|
onPartialResult: async (result) => {
|
|
9355
9546
|
try {
|
|
9356
|
-
await postBatchHealPartial(serverUrl, headers, runId, result);
|
|
9547
|
+
await postBatchHealPartial(serverUrl, headers, runId, result, claimEpoch);
|
|
9357
9548
|
} catch (err) {
|
|
9358
9549
|
warn(`batch-heal partial post failed for ${result.testCaseId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
9359
9550
|
}
|
|
@@ -9361,6 +9552,28 @@ function batchHealPartialHooks(serverUrl, headers, runId, projectId, warn = (msg
|
|
|
9361
9552
|
resumeLookup: () => fetchBatchHealAppliedIds(serverUrl, headers, runId, projectId)
|
|
9362
9553
|
};
|
|
9363
9554
|
}
|
|
9555
|
+
function batchHealNoStart(tests, baseUrl) {
|
|
9556
|
+
if (tests.length === 0) {
|
|
9557
|
+
return {
|
|
9558
|
+
body: { perTest: [], systemicDiagnosis: "batch-heal dispatched with empty tests list", logs: "no tests to heal", durationMs: 0 },
|
|
9559
|
+
level: "warn",
|
|
9560
|
+
logLine: "batch-heal: empty payload, posting no-op result"
|
|
9561
|
+
};
|
|
9562
|
+
}
|
|
9563
|
+
if (!baseUrl) {
|
|
9564
|
+
return {
|
|
9565
|
+
body: {
|
|
9566
|
+
perTest: [],
|
|
9567
|
+
systemicDiagnosis: "batch-heal aborted: BASE_URL missing for project (set BASE_URL in Test Data or FrameworkRepo.baseUrl)",
|
|
9568
|
+
logs: "batch-heal aborted: empty baseUrl",
|
|
9569
|
+
durationMs: 0
|
|
9570
|
+
},
|
|
9571
|
+
level: "fail",
|
|
9572
|
+
logLine: "batch-heal aborted \u2014 BASE_URL is empty in /pending payload"
|
|
9573
|
+
};
|
|
9574
|
+
}
|
|
9575
|
+
return null;
|
|
9576
|
+
}
|
|
9364
9577
|
var LOG_APPEND_MAX_LINES = 100;
|
|
9365
9578
|
var LOG_APPEND_MAX_LINE_LEN = 8 * 1024;
|
|
9366
9579
|
var LOG_APPEND_MAX_BODY_BYTES = 200 * 1024;
|
|
@@ -9427,18 +9640,25 @@ function createHealLogStreamer(serverUrl, headers, runId) {
|
|
|
9427
9640
|
};
|
|
9428
9641
|
return { onLog, flushNow };
|
|
9429
9642
|
}
|
|
9430
|
-
async function postShedOn413(send, body, label, ladder = import_result_shed.CAPTURE_SHED_LADDER, progress) {
|
|
9643
|
+
async function postShedOn413(send, body, label, ladder = import_result_shed.CAPTURE_SHED_LADDER, progress, onPlain413) {
|
|
9431
9644
|
let current = progress?.body ?? body;
|
|
9432
9645
|
let fromRung = progress?.fromRung ?? 0;
|
|
9433
9646
|
let res = await send(current);
|
|
9434
|
-
while (res.status === 413
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9647
|
+
while (res.status === 413) {
|
|
9648
|
+
if (await (0, import_result_api_call_bodies2.isResultStructureTooCostlyResponse)(res)) {
|
|
9649
|
+
const next = (0, import_result_shed.nextShedPayload)(current, ladder, fromRung);
|
|
9650
|
+
if (!next) break;
|
|
9651
|
+
console.warn(
|
|
9652
|
+
`[runner] Server refused ${label} as too costly to parse (413 ${import_result_api_call_bodies2.RESULT_STRUCTURE_TOO_COSTLY}); retrying a lighter form (shed level ${next.level}, ${next.shed} item${next.shed === 1 ? "" : "s"} removed).`
|
|
9653
|
+
);
|
|
9654
|
+
current = next.payload;
|
|
9655
|
+
fromRung = next.nextRung;
|
|
9656
|
+
} else {
|
|
9657
|
+
const lighter = onPlain413?.(current) ?? null;
|
|
9658
|
+
if (!lighter) break;
|
|
9659
|
+
console.warn(`[runner] Server refused ${label} as too large (413); retrying it without its media.`);
|
|
9660
|
+
current = lighter;
|
|
9661
|
+
}
|
|
9442
9662
|
if (progress) {
|
|
9443
9663
|
progress.body = current;
|
|
9444
9664
|
progress.fromRung = fromRung;
|
|
@@ -9457,22 +9677,24 @@ async function postCaptureResult(serverUrl, headers, runId, result, backoffBaseM
|
|
|
9457
9677
|
})), progress.body, "capture result", import_result_shed.CAPTURE_SHED_LADDER, progress), "capture result", backoffBaseMs);
|
|
9458
9678
|
throwUnlessResultLanded(res, runId, "capture result");
|
|
9459
9679
|
}
|
|
9460
|
-
async function postRegressionResult(serverUrl, headers, runId, body, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS) {
|
|
9461
|
-
const
|
|
9680
|
+
async function postRegressionResult(serverUrl, headers, runId, body, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS, claimEpoch) {
|
|
9681
|
+
const fit = (0, import_result_shed.fitRegressionScreenshots)(withBrowsersPathHint(body));
|
|
9682
|
+
if (fit.dropped > 0) console.warn(`[runner] Regression result over the route's body limit: ${fit.dropped} failing spec(s)' screenshot left out.`);
|
|
9683
|
+
const progress = { body: fit.payload, fromRung: 0 };
|
|
9462
9684
|
const res = await postRetryingTransient(() => postShedOn413((payload) => fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/regression-result`, () => ({
|
|
9463
9685
|
method: "POST",
|
|
9464
9686
|
headers,
|
|
9465
|
-
body: JSON.stringify(payload),
|
|
9687
|
+
body: JSON.stringify(withClaimEpoch(payload, claimEpoch)),
|
|
9466
9688
|
signal: AbortSignal.timeout(6e4)
|
|
9467
|
-
})), progress.body, "regression result", import_result_shed.REGRESSION_SHED_LADDER, progress), "regression result", backoffBaseMs);
|
|
9689
|
+
})), progress.body, "regression result", import_result_shed.REGRESSION_SHED_LADDER, progress, import_result_shed.withoutRegressionScreenshots), "regression result", backoffBaseMs);
|
|
9468
9690
|
throwUnlessResultLanded(res, runId, "regression result");
|
|
9469
9691
|
}
|
|
9470
|
-
function postLiveSuiteResult(serverUrl, headers, runId, body, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS) {
|
|
9692
|
+
function postLiveSuiteResult(serverUrl, headers, runId, body, backoffBaseMs = RESULT_POST_BACKOFF_BASE_MS, claimEpoch) {
|
|
9471
9693
|
const progress = { body, fromRung: 0 };
|
|
9472
9694
|
return postRetryingTransient(() => postShedOn413((payload) => fetchRetrying429(`${serverUrl}/api/runner/runs/${runId}/live-suite-result`, () => ({
|
|
9473
9695
|
method: "POST",
|
|
9474
9696
|
headers,
|
|
9475
|
-
body: JSON.stringify(payload),
|
|
9697
|
+
body: JSON.stringify(withClaimEpoch(payload, claimEpoch)),
|
|
9476
9698
|
signal: AbortSignal.timeout(6e4)
|
|
9477
9699
|
})), progress.body, "live suite result", import_result_shed.LIVE_SUITE_SHED_LADDER, progress), "live suite result", backoffBaseMs);
|
|
9478
9700
|
}
|
|
@@ -9688,6 +9910,12 @@ function pruneCollectedPageForPost(page) {
|
|
|
9688
9910
|
if (Array.isArray(out.observations)) out.observations = out.observations.slice(-200).map((obs) => compactForAudit(obs));
|
|
9689
9911
|
return out;
|
|
9690
9912
|
}
|
|
9913
|
+
function discoveryRenderTypeFields(input) {
|
|
9914
|
+
if (input.err instanceof import_runner_core7.UnsupportedRendererError) {
|
|
9915
|
+
return { detectedAppRenderType: input.err.renderType, appRenderDetectionSignals: input.err.signals };
|
|
9916
|
+
}
|
|
9917
|
+
return input.appRenderType ? { detectedAppRenderType: input.appRenderType } : {};
|
|
9918
|
+
}
|
|
9691
9919
|
function prepareDiscoveryResultForPost(result, logs2, options = {}) {
|
|
9692
9920
|
const safe = { ...result, logs: truncateTextTail(logs2, RESULT_LOG_MAX_CHARS, "discovery result log") };
|
|
9693
9921
|
if (Array.isArray(safe.collectedPages)) safe.collectedPages = safe.collectedPages.map(pruneCollectedPageForPost);
|
|
@@ -9864,15 +10092,16 @@ function toLiveBrowserMode(mode) {
|
|
|
9864
10092
|
async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = () => {
|
|
9865
10093
|
}, onMobileHealPending = () => {
|
|
9866
10094
|
}, assignedDeviceId, mobileLeaseError, signal) {
|
|
9867
|
-
if (signal && (0,
|
|
9868
|
-
return (0,
|
|
10095
|
+
if (signal && (0, import_runner_core9.currentRunAbortSignal)() !== signal) {
|
|
10096
|
+
return (0, import_runner_core9.runWithAbortScope)({ signal, runId: run2.runId }, () => executeRun(run2, opts, headers, requestLiveBrowserHeartbeat, onMobileHealPending, assignedDeviceId, mobileLeaseError, signal));
|
|
9869
10097
|
}
|
|
9870
10098
|
const runMode = run2.mode ?? "run";
|
|
9871
10099
|
const RUNNER_MODE = opts.runnerMode ?? "playwright-native";
|
|
9872
10100
|
const runLabel = run2.testCaseId ?? run2.testName ?? (runMode === "run" ? "auth-setup" : runMode);
|
|
9873
10101
|
const modeLabel = runMode === "run" ? RUNNER_MODE : runMode;
|
|
9874
10102
|
log.info(`Running test: ${runLabel} (run ${run2.runId}) [mode: ${modeLabel}]`);
|
|
9875
|
-
|
|
10103
|
+
let tlsCertNote = Promise.resolve(null);
|
|
10104
|
+
const postRunResult = async (result) => postResult(opts.serverUrl, headers, run2.runId, (0, import_tls_cert_probe.withRunNote)(result, await (0, import_tls_cert_probe.noteWithinCap)(tlsCertNote)), run2.claimEpoch);
|
|
9876
10105
|
const browserlessRepositoryRead = (0, import_live_browser_registry.isBrowserlessRepositoryRead)(run2.discoveryContext?.frameworkImport?.kind);
|
|
9877
10106
|
const liveBrowserHandle = browserlessRepositoryRead ? import_live_browser_registry.NO_LIVE_BROWSER : import_live_browser_registry.liveBrowserRegistry.register({
|
|
9878
10107
|
sessionId: `${run2.runId}`,
|
|
@@ -9920,10 +10149,12 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
9920
10149
|
authStandingMode: authPlan.standingHeadersMode,
|
|
9921
10150
|
storageStatePath: wrapperStorageStatePath,
|
|
9922
10151
|
apiBaseUrl: run2.apiBaseUrl,
|
|
10152
|
+
// The run's own @api tag: an imported API spec's code may not say so (see executeNativeOnce's apiTest).
|
|
10153
|
+
apiTest: Array.isArray(run2.tags) && run2.tags.includes("@api"),
|
|
9923
10154
|
...nativeOptions,
|
|
9924
10155
|
liveBrowserHandle,
|
|
9925
10156
|
onLiveBrowserSnapshot: requestLiveBrowserHeartbeat,
|
|
9926
|
-
signal: (0,
|
|
10157
|
+
signal: (0, import_runner_core9.linkRunAbort)(nativeOptions.signal) ?? signal
|
|
9927
10158
|
});
|
|
9928
10159
|
const callbackContext = {
|
|
9929
10160
|
runId: run2.runId,
|
|
@@ -9936,7 +10167,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
9936
10167
|
};
|
|
9937
10168
|
const startTime = Date.now();
|
|
9938
10169
|
try {
|
|
9939
|
-
(0,
|
|
10170
|
+
(0, import_runner_core9.throwIfRunAborted)();
|
|
9940
10171
|
if (mobileLeaseError) {
|
|
9941
10172
|
await postRunResult({
|
|
9942
10173
|
status: "ERROR",
|
|
@@ -9994,6 +10225,10 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
9994
10225
|
} else if (preflight.note) {
|
|
9995
10226
|
log.info(preflight.note);
|
|
9996
10227
|
}
|
|
10228
|
+
tlsCertNote = (0, import_tls_cert_probe.targetCertificateNote)(run2.baseUrl);
|
|
10229
|
+
void tlsCertNote.then((note) => {
|
|
10230
|
+
if (note) log.warn(note);
|
|
10231
|
+
});
|
|
9997
10232
|
}
|
|
9998
10233
|
if (runMode === "auth-setup") {
|
|
9999
10234
|
if (!run2.playwrightCode) {
|
|
@@ -10052,7 +10287,7 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10052
10287
|
releaseAuthLock2
|
|
10053
10288
|
);
|
|
10054
10289
|
const duration2 = Date.now() - startTime;
|
|
10055
|
-
(0,
|
|
10290
|
+
(0, import_runner_core9.throwIfRunAborted)();
|
|
10056
10291
|
await postRunResult({
|
|
10057
10292
|
status: result2.passed ? "PASSED" : "FAILED",
|
|
10058
10293
|
apiCalls: result2.apiCalls,
|
|
@@ -10072,14 +10307,10 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10072
10307
|
if (runMode === "batch-heal") {
|
|
10073
10308
|
const tests = run2.batchHealTests ?? [];
|
|
10074
10309
|
log.info(`Heal engine: GROK BATCH (${tests.length} tests, healerModel=${run2.healerModel ?? "null"})`);
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
logs: "no tests to heal",
|
|
10080
|
-
durationMs: 0
|
|
10081
|
-
});
|
|
10082
|
-
log.warn("batch-heal: empty payload, posting no-op result");
|
|
10310
|
+
const noStart = batchHealNoStart(tests, run2.baseUrl);
|
|
10311
|
+
if (noStart) {
|
|
10312
|
+
await postBatchHealResult(opts.serverUrl, headers, run2.runId, noStart.body, RESULT_POST_BACKOFF_BASE_MS, run2.claimEpoch);
|
|
10313
|
+
log[noStart.level](noStart.logLine);
|
|
10083
10314
|
return;
|
|
10084
10315
|
}
|
|
10085
10316
|
const healLogStream = createHealLogStreamer(opts.serverUrl, headers, run2.runId);
|
|
@@ -10101,10 +10332,10 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10101
10332
|
completionTokens: info.completionTokens,
|
|
10102
10333
|
totalTokens: info.totalTokens,
|
|
10103
10334
|
requestBody: { iteration: info.iteration, toolCalls: info.toolCalls, session: info.requestMessages?.[0] ?? null },
|
|
10104
|
-
metadata: { phase: "heal", groupId: info.groupId, iteration: info.iteration, healMode: "batch", ...(0,
|
|
10335
|
+
metadata: { phase: "heal", groupId: info.groupId, iteration: info.iteration, healMode: "batch", ...(0, import_runner_core9.cliAuditMetadata)(info) }
|
|
10105
10336
|
});
|
|
10106
10337
|
};
|
|
10107
|
-
const batchResult = await (0,
|
|
10338
|
+
const batchResult = await (0, import_runner_core9.executeGrokPerBatchHeal)(tests, run2.baseUrl, {
|
|
10108
10339
|
onAICall: batchHealAudit,
|
|
10109
10340
|
testVariables: run2.testVariables,
|
|
10110
10341
|
runNativeTest: runNativeWithLiveBrowser,
|
|
@@ -10113,9 +10344,9 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10113
10344
|
saveRefreshedLogin: grokLogin?.saveRefreshed,
|
|
10114
10345
|
engineSpec: run2.healerModel,
|
|
10115
10346
|
// Returns HEAL_TIMEOUT: for the tests not reached instead of dying at the runner's batch-heal deadline.
|
|
10116
|
-
deadlineAt: (0,
|
|
10347
|
+
deadlineAt: (0, import_runner_core7.batchHealDeadlineAt)(startTime),
|
|
10117
10348
|
onLog: healLogStream.onLog,
|
|
10118
|
-
...batchHealPartialHooks(opts.serverUrl, headers, run2.runId, run2.projectId || void 0)
|
|
10349
|
+
...batchHealPartialHooks(opts.serverUrl, headers, run2.runId, run2.projectId || void 0, void 0, run2.claimEpoch)
|
|
10119
10350
|
});
|
|
10120
10351
|
await healLogStream.flushNow();
|
|
10121
10352
|
try {
|
|
@@ -10123,15 +10354,15 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10123
10354
|
} catch (drainErr) {
|
|
10124
10355
|
console.warn(`[batch-heal] audit flush failed: ${drainErr instanceof Error ? drainErr.message : String(drainErr)}`);
|
|
10125
10356
|
}
|
|
10126
|
-
(0,
|
|
10357
|
+
(0, import_runner_core9.throwIfRunAborted)();
|
|
10127
10358
|
try {
|
|
10128
10359
|
await postBatchHealResult(opts.serverUrl, headers, run2.runId, {
|
|
10129
10360
|
perTest: batchResult.perTest,
|
|
10130
10361
|
systemicDiagnosis: batchResult.systemicDiagnosis,
|
|
10131
10362
|
logs: batchResult.logs,
|
|
10132
10363
|
durationMs: batchResult.totalDurationMs
|
|
10133
|
-
});
|
|
10134
|
-
log.ok((0,
|
|
10364
|
+
}, RESULT_POST_BACKOFF_BASE_MS, run2.claimEpoch);
|
|
10365
|
+
log.ok((0, import_runner_core9.batchHealCompleteLine)(batchResult.perTest, tests.length, batchResult.totalDurationMs));
|
|
10135
10366
|
} catch (err) {
|
|
10136
10367
|
log.fail(`Failed to post batch heal result: ${err instanceof Error ? err.message : String(err)}`);
|
|
10137
10368
|
}
|
|
@@ -10164,6 +10395,8 @@ async function executeRun(run2, opts, headers, requestLiveBrowserHeartbeat = ()
|
|
|
10164
10395
|
}, run2.runId) : void 0;
|
|
10165
10396
|
const apiHealStorageStatePath = apiHealAuthPaths ? (0, import_local_auth_state.getValidLocalAuthState)(apiHealAuthPaths.cachePath) : void 0;
|
|
10166
10397
|
if (apiHealStorageStatePath) log.info("API heal verification is using saved auth state");
|
|
10398
|
+
const apiHealAuthSession = (0, import_runner_core8.sharedAuthSessionStamp)(apiHealStorageStatePath, false, sharedAuthInjectEnabled());
|
|
10399
|
+
const apiHealAuthPlan = authPlanFor(run2.tags, run2.playwrightCode);
|
|
10167
10400
|
const stickyAppBugs = /* @__PURE__ */ new Map();
|
|
10168
10401
|
const lastChangeByTitle = /* @__PURE__ */ new Map();
|
|
10169
10402
|
let currentError = run2.lastFailureError ?? "Test failed";
|
|
@@ -10278,25 +10511,42 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10278
10511
|
continue;
|
|
10279
10512
|
}
|
|
10280
10513
|
if (result3.verdict === "app_bug") {
|
|
10281
|
-
log.warn(` APP BUG detected (legacy schema): ${result3.bugDescription}`);
|
|
10282
10514
|
const duration4 = Date.now() - startTime;
|
|
10515
|
+
const legacyCold = (0, import_runner_core8.apiHealColdSessionRelabel)("legacy", {
|
|
10516
|
+
tags: run2.tags ?? [],
|
|
10517
|
+
authStateExpected: apiHealAuthPlan.needsCookieState,
|
|
10518
|
+
authStateInjected: (0, import_runner_core8.storageStateDelivered)(apiHealStorageStatePath, finalVerifyResult),
|
|
10519
|
+
needsStandingHeaders: apiHealAuthPlan.needsStandingHeaders,
|
|
10520
|
+
standingHeadersInjected: !!finalVerifyResult?.authDelivery?.standingHeadersInjected,
|
|
10521
|
+
error: result3.bugDescription ?? null,
|
|
10522
|
+
logs: finalVerifyResult?.logs ?? null,
|
|
10523
|
+
apiCalls: null
|
|
10524
|
+
});
|
|
10525
|
+
if (legacyCold) {
|
|
10526
|
+
log.warn(legacyCold.logLine);
|
|
10527
|
+
await postRunResult({ status: legacyCold.status, error: legacyCold.error, duration: duration4, authSession: apiHealAuthSession });
|
|
10528
|
+
return;
|
|
10529
|
+
}
|
|
10530
|
+
log.warn(` APP BUG detected (legacy schema): ${result3.bugDescription}`);
|
|
10283
10531
|
await postRunResult({
|
|
10284
10532
|
status: "FAILED",
|
|
10285
10533
|
error: `APP_BUG: ${result3.bugDescription}`,
|
|
10286
|
-
duration: duration4
|
|
10534
|
+
duration: duration4,
|
|
10535
|
+
authSession: apiHealAuthSession
|
|
10287
10536
|
});
|
|
10288
10537
|
return;
|
|
10289
10538
|
}
|
|
10290
10539
|
if (result3.verdict === "unfixable") {
|
|
10291
10540
|
log.fail(` Unfixable: ${result3.reason}`);
|
|
10292
|
-
finalUnfixableReason = result3.reason;
|
|
10541
|
+
finalUnfixableReason = result3.reason ?? "AI could not fix the test";
|
|
10293
10542
|
break;
|
|
10294
10543
|
}
|
|
10295
10544
|
lastHealedCode = result3.playwrightCode ?? result3.healedCode;
|
|
10296
10545
|
lastChangeDescription = result3.changeDescription;
|
|
10297
10546
|
if (!lastHealedCode) {
|
|
10298
|
-
log.
|
|
10299
|
-
|
|
10547
|
+
log.warn(` Attempt ${attempt}: AI returned fix verdict but no code \u2014 retrying with prompt`);
|
|
10548
|
+
currentError = `Attempt ${attempt} failed: You described a fix but did not include the \`playwrightCode\` field. Please provide the complete updated test file as \`playwrightCode\`.`;
|
|
10549
|
+
continue;
|
|
10300
10550
|
}
|
|
10301
10551
|
finalVerifyResult = await runNativeWithLiveBrowser({
|
|
10302
10552
|
playwrightCode: lastHealedCode,
|
|
@@ -10353,7 +10603,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10353
10603
|
let apiHealStatus = passed ? "PASSED" : "FAILED";
|
|
10354
10604
|
let apiHealError = composedError;
|
|
10355
10605
|
if (!passed) {
|
|
10356
|
-
if (finalUnfixableReason?.startsWith(
|
|
10606
|
+
if (finalUnfixableReason?.startsWith(import_runner_core7.HEAL_AI_NO_ANSWER_ERROR_PREFIX) || (0, import_runner_core7.isHealAiQuotaError)(finalUnfixableReason)) {
|
|
10357
10607
|
apiHealStatus = "ERROR";
|
|
10358
10608
|
apiHealError = finalUnfixableReason;
|
|
10359
10609
|
} else if (isConfigLikeReason(classifyErrorMessage(composedError ?? null))) {
|
|
@@ -10390,6 +10640,23 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10390
10640
|
}
|
|
10391
10641
|
}
|
|
10392
10642
|
}
|
|
10643
|
+
if (apiHealStatus === "FAILED") {
|
|
10644
|
+
const cold = (0, import_runner_core8.apiHealColdSessionRelabel)("verified", {
|
|
10645
|
+
tags: run2.tags ?? [],
|
|
10646
|
+
authStateExpected: apiHealAuthPlan.needsCookieState,
|
|
10647
|
+
authStateInjected: (0, import_runner_core8.storageStateDelivered)(apiHealStorageStatePath, finalVerifyResult),
|
|
10648
|
+
needsStandingHeaders: apiHealAuthPlan.needsStandingHeaders,
|
|
10649
|
+
standingHeadersInjected: !!finalVerifyResult?.authDelivery?.standingHeadersInjected,
|
|
10650
|
+
error: apiHealError ?? finalVerifyResult?.error,
|
|
10651
|
+
logs: finalVerifyResult?.logs,
|
|
10652
|
+
apiCalls: rawSteps.flatMap((s) => s.apiCalls ?? [])
|
|
10653
|
+
});
|
|
10654
|
+
if (cold) {
|
|
10655
|
+
apiHealStatus = cold.status;
|
|
10656
|
+
apiHealError = cold.error;
|
|
10657
|
+
log.warn(`${cold.logLine} (was "${composedError?.slice(0, 60)}\u2026")`);
|
|
10658
|
+
}
|
|
10659
|
+
}
|
|
10393
10660
|
await postHealOutcome({
|
|
10394
10661
|
postRunResult: () => postRunResult({
|
|
10395
10662
|
status: apiHealStatus,
|
|
@@ -10401,9 +10668,10 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10401
10668
|
// Preserve healedCode on ALL heal outcomes - including app-bug-only -
|
|
10402
10669
|
// so the file with test.fixme wraps gets committed and audited.
|
|
10403
10670
|
healedCode: lastHealedCode,
|
|
10404
|
-
duration: duration3
|
|
10671
|
+
duration: duration3,
|
|
10672
|
+
authSession: apiHealAuthSession
|
|
10405
10673
|
}),
|
|
10406
|
-
postHealResult: (healedCode, testCaseId) => postHealResult(opts.serverUrl, headers, run2.runId, healedCode, testCaseId),
|
|
10674
|
+
postHealResult: (healedCode, testCaseId) => postHealResult(opts.serverUrl, headers, run2.runId, healedCode, testCaseId, void 0, void 0, run2.claimEpoch),
|
|
10407
10675
|
passed,
|
|
10408
10676
|
healedCode: lastHealedCode,
|
|
10409
10677
|
testCaseId: run2.testCaseId,
|
|
@@ -10439,11 +10707,11 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10439
10707
|
completionTokens: info.completionTokens,
|
|
10440
10708
|
totalTokens: info.totalTokens,
|
|
10441
10709
|
...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
|
|
10442
|
-
metadata: { groupId: info.groupId, iteration: info.iteration, testName: run2.testCaseId, ...(0,
|
|
10710
|
+
metadata: { groupId: info.groupId, iteration: info.iteration, testName: run2.testCaseId, ...(0, import_runner_core9.cliAuditMetadata)(info) }
|
|
10443
10711
|
});
|
|
10444
10712
|
};
|
|
10445
10713
|
const envOverride = (process.env.USE_GROK_PER_TEST_HEAL ?? "").trim().toLowerCase();
|
|
10446
|
-
const useGrokHeal = envOverride === "true" ? true : envOverride === "false" ? false : (0,
|
|
10714
|
+
const useGrokHeal = envOverride === "true" ? true : envOverride === "false" ? false : (0, import_runner_core9.getAgenticEngine)(run2.healerModel) === "grok-build";
|
|
10447
10715
|
const healOpts = {
|
|
10448
10716
|
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
10449
10717
|
tags: run2.tags ?? [],
|
|
@@ -10466,19 +10734,19 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10466
10734
|
// Whole-heal deadline: the runner's heal deadline minus a margin for the result
|
|
10467
10735
|
// post (heal-deadline.ts). The healer stops both phases inside it and returns a
|
|
10468
10736
|
// HEAL_TIMEOUT: result instead of dying as a reclaimed task.
|
|
10469
|
-
deadlineAt: (0,
|
|
10737
|
+
deadlineAt: (0, import_runner_core7.healDeadlineAt)(startTime)
|
|
10470
10738
|
};
|
|
10471
10739
|
if (useGrokHeal) {
|
|
10472
10740
|
const source = envOverride === "true" ? "env USE_GROK_PER_TEST_HEAL=true" : `healerModel=${run2.healerModel ?? "null"}`;
|
|
10473
10741
|
log.info(`Heal engine: GROK (${source})`);
|
|
10474
10742
|
}
|
|
10475
10743
|
const grokHealLogStream = useGrokHeal ? createHealLogStreamer(opts.serverUrl, headers, run2.runId) : null;
|
|
10476
|
-
const httpFallback = run2.healerFallbackModel && (0,
|
|
10744
|
+
const httpFallback = run2.healerFallbackModel && (0, import_runner_core9.getAgenticEngine)(run2.healerFallbackModel) === "native" ? run2.healerFallbackModel : null;
|
|
10477
10745
|
let result2;
|
|
10478
10746
|
if (useGrokHeal) {
|
|
10479
10747
|
try {
|
|
10480
10748
|
const grokLogin = await (0, import_fetch_cli_subscription.fetchCliSubscription)({ serverUrl: opts.serverUrl, runId: run2.runId, headers, cli: "grok-build", onLog: grokHealLogStream?.onLog });
|
|
10481
|
-
result2 = await (0,
|
|
10749
|
+
result2 = await (0, import_runner_core9.executeGrokPerTestHeal)(run2.playwrightCode, run2.steps, run2.baseUrl, {
|
|
10482
10750
|
...healOpts,
|
|
10483
10751
|
subscriptionSecret: grokLogin?.value,
|
|
10484
10752
|
saveRefreshedLogin: grokLogin?.saveRefreshed,
|
|
@@ -10523,28 +10791,42 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10523
10791
|
console.warn(`[runner]${auditDrainError}`);
|
|
10524
10792
|
}
|
|
10525
10793
|
const isConfigIssue = !result2.passed && isConfigLikeReason(classifyErrorMessage(result2.error ?? null));
|
|
10526
|
-
let healStatus = (0,
|
|
10794
|
+
let healStatus = (0, import_runner_core7.healRunStatus)({ passed: result2.passed, error: result2.error, isConfigIssue });
|
|
10527
10795
|
let healError = result2.error;
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10796
|
+
const rateLimitRelabel = (0, import_runner_core8.uiHealRateLimitRelabel)({
|
|
10797
|
+
passed: result2.passed,
|
|
10798
|
+
isConfigIssue,
|
|
10799
|
+
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
10800
|
+
tags: run2.tags ?? [],
|
|
10801
|
+
error: result2.error,
|
|
10802
|
+
logs: result2.logs,
|
|
10803
|
+
stepResults: result2.stepResults,
|
|
10804
|
+
apiCalls: result2.apiCalls
|
|
10805
|
+
});
|
|
10806
|
+
if (rateLimitRelabel) {
|
|
10807
|
+
healStatus = rateLimitRelabel.status;
|
|
10808
|
+
healError = rateLimitRelabel.error;
|
|
10809
|
+
log.warn(rateLimitRelabel.logLine);
|
|
10810
|
+
}
|
|
10811
|
+
const loginRelabel = (0, import_runner_core8.uiHealLoginRelabel)({
|
|
10812
|
+
status: healStatus,
|
|
10813
|
+
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
10814
|
+
tags: run2.tags ?? [],
|
|
10815
|
+
error: healError,
|
|
10816
|
+
logs: result2.logs,
|
|
10817
|
+
stepResults: result2.stepResults
|
|
10818
|
+
});
|
|
10819
|
+
if (loginRelabel) {
|
|
10820
|
+
healStatus = loginRelabel.status;
|
|
10821
|
+
healError = loginRelabel.error;
|
|
10822
|
+
log.warn(loginRelabel.logLine);
|
|
10542
10823
|
}
|
|
10543
10824
|
await postHealOutcome({
|
|
10544
10825
|
postRunResult: () => postRunResult({
|
|
10545
10826
|
status: healStatus,
|
|
10546
10827
|
stepResults: result2.stepResults,
|
|
10547
10828
|
screenshots: result2.screenshots,
|
|
10829
|
+
video: result2.video,
|
|
10548
10830
|
apiCalls: result2.apiCalls,
|
|
10549
10831
|
logs: (result2.logs ?? "") + auditDrainError,
|
|
10550
10832
|
error: healError,
|
|
@@ -10565,7 +10847,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10565
10847
|
});
|
|
10566
10848
|
if (result2.logs) {
|
|
10567
10849
|
const phaseLines = result2.logs.split("\n").filter(
|
|
10568
|
-
(l) => l.includes("Phase 1: Script-driven") || l.includes("Phase 2: Escalating") || l.includes("Phase 2: Script + MCP") || l.includes("Phase 1 succeeded") || l.includes("Phase 2 iteration") || l.includes("\u{1F916} Verdict:") || l.includes("\u{1F916} finish_fix:") || l.includes("native verification PASSED") || l.includes("native verification FAILED") || l.includes("Native run PASSED") || l.includes("Native run FAILED") || l.includes("APP_BUG") || l.includes("CONFIG_ISSUE") || l.includes("needs_browser") || l.includes("--- Attempt") || l.includes("Proven commands") || l.includes("Proven fix:") || l.includes("Same error repeated") || l.includes("aborting heal")
|
|
10850
|
+
(l) => l.includes("Phase 1: Script-driven") || l.includes("Phase 2: Escalating") || l.includes("Phase 2: Script + MCP") || l.includes("Phase 1 succeeded") || l.includes("Phase 2 iteration") || l.includes("\u{1F916} Verdict:") || l.includes("\u{1F916} finish_fix:") || l.includes("Preflight:") || l.includes("native verification PASSED") || l.includes("native verification FAILED") || l.includes("Native run PASSED") || l.includes("Native run FAILED") || l.includes("APP_BUG") || l.includes("CONFIG_ISSUE") || l.includes("needs_browser") || l.includes("--- Attempt") || l.includes("Proven commands") || l.includes("Proven fix:") || l.includes("Same error repeated") || l.includes("aborting heal")
|
|
10569
10851
|
);
|
|
10570
10852
|
if (phaseLines.length > 0) {
|
|
10571
10853
|
console.log(`${C2.gray}\u2500\u2500 Heal summary \u2500\u2500${C2.reset}`);
|
|
@@ -10578,7 +10860,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10578
10860
|
);
|
|
10579
10861
|
return;
|
|
10580
10862
|
}
|
|
10581
|
-
if (runMode === "regression-suite") {
|
|
10863
|
+
if (runMode === "regression-suite" || runMode === "system-regression-suite") {
|
|
10582
10864
|
if (!run2.repoOwner || !run2.repoName) {
|
|
10583
10865
|
await postRunResult({ status: "ERROR", error: "No framework repo configured" });
|
|
10584
10866
|
log.fail("ERROR - No framework repo configured");
|
|
@@ -10600,8 +10882,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10600
10882
|
// A cancel, a newer claim or a shutdown kills the suite instead of letting it run on for up to 85 min.
|
|
10601
10883
|
signal
|
|
10602
10884
|
});
|
|
10603
|
-
const overall =
|
|
10604
|
-
(0,
|
|
10885
|
+
const overall = (0, import_regression_runner.regressionSuiteOverall)(result2);
|
|
10886
|
+
(0, import_runner_core9.throwIfRunAborted)();
|
|
10605
10887
|
await postRegressionResult(opts.serverUrl, headers, run2.runId, {
|
|
10606
10888
|
overall,
|
|
10607
10889
|
logs: result2.logs,
|
|
@@ -10609,7 +10891,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10609
10891
|
error: result2.error,
|
|
10610
10892
|
testResults: result2.testResults,
|
|
10611
10893
|
skippedSpecs: result2.skippedSpecs
|
|
10612
|
-
});
|
|
10894
|
+
}, RESULT_POST_BACKOFF_BASE_MS, run2.claimEpoch);
|
|
10613
10895
|
log[result2.passed ? "ok" : "fail"](
|
|
10614
10896
|
`Regression ${result2.passed ? "PASSED" : "FAILED"} (${result2.duration}ms) - ${result2.testResults.length} specs`
|
|
10615
10897
|
);
|
|
@@ -10631,9 +10913,9 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10631
10913
|
`${opts.serverUrl}/api/runner/runs/${run2.runId}/ai-audit-batch`,
|
|
10632
10914
|
headers
|
|
10633
10915
|
);
|
|
10634
|
-
const suiteQuotaFallback = (0,
|
|
10916
|
+
const suiteQuotaFallback = (0, import_runner_core7.createQuotaFallbackState)(run2.quotaFallbackModel);
|
|
10635
10917
|
for (const failedTest of failedTests) {
|
|
10636
|
-
(0,
|
|
10918
|
+
(0, import_runner_core9.throwIfRunAborted)();
|
|
10637
10919
|
const testStart = Date.now();
|
|
10638
10920
|
log.info(` Healing: ${failedTest.name ?? failedTest.testCaseId}`);
|
|
10639
10921
|
const isApiTest = Array.isArray(failedTest.tags) && failedTest.tags.includes("@api");
|
|
@@ -10645,8 +10927,18 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10645
10927
|
body: JSON.stringify({ error: String(failedTest.error ?? "").slice(0, 5e5), testCaseId: failedTest.testCaseId }),
|
|
10646
10928
|
signal: AbortSignal.timeout(6e4)
|
|
10647
10929
|
});
|
|
10648
|
-
|
|
10649
|
-
|
|
10930
|
+
const healBody = healRes.ok ? await healRes.json() : null;
|
|
10931
|
+
if (healBody?.verdict === "unfixable" && (0, import_runner_core7.isHealAiQuotaError)(healBody.reason)) {
|
|
10932
|
+
results.push({
|
|
10933
|
+
testCaseId: failedTest.testCaseId,
|
|
10934
|
+
status: "unfixable",
|
|
10935
|
+
error: healBody.reason,
|
|
10936
|
+
diagnosis: healBody.reason,
|
|
10937
|
+
duration: Date.now() - testStart
|
|
10938
|
+
});
|
|
10939
|
+
log.warn(` Not healed (AI provider quota): ${failedTest.name}`);
|
|
10940
|
+
} else if (healBody) {
|
|
10941
|
+
const { healedCode } = healBody;
|
|
10650
10942
|
const failedTestPlan = authPlanFor(failedTest.tags, healedCode);
|
|
10651
10943
|
const verifyResult = await runNativeWithLiveBrowser({
|
|
10652
10944
|
playwrightCode: healedCode,
|
|
@@ -10701,12 +10993,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10701
10993
|
completionTokens: info.completionTokens,
|
|
10702
10994
|
totalTokens: info.totalTokens,
|
|
10703
10995
|
...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
|
|
10704
|
-
metadata: { groupId: info.groupId, testCaseId: failedTest.testCaseId, ...(0,
|
|
10996
|
+
metadata: { groupId: info.groupId, testCaseId: failedTest.testCaseId, ...(0, import_runner_core9.cliAuditMetadata)(info) }
|
|
10705
10997
|
});
|
|
10706
10998
|
};
|
|
10707
10999
|
try {
|
|
10708
11000
|
const envOverrideSuite = (process.env.USE_GROK_PER_TEST_HEAL ?? "").trim().toLowerCase();
|
|
10709
|
-
const useGrokHealSuite = envOverrideSuite === "true" ? true : envOverrideSuite === "false" ? false : (0,
|
|
11001
|
+
const useGrokHealSuite = envOverrideSuite === "true" ? true : envOverrideSuite === "false" ? false : (0, import_runner_core9.getAgenticEngine)(run2.healerModel) === "grok-build";
|
|
10710
11002
|
const suiteHealOpts = {
|
|
10711
11003
|
testName: failedTest.name ?? failedTest.testCaseId,
|
|
10712
11004
|
tags: failedTest.tags ?? [],
|
|
@@ -10714,6 +11006,10 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10714
11006
|
healingContext: failedTest.healingContext ?? null,
|
|
10715
11007
|
selectorRegistryContext: failedTest.selectorRegistryContext ?? null,
|
|
10716
11008
|
lastFailureError: failedTest.error,
|
|
11009
|
+
// A suite heal reports fixed / app-bug / config-issue / unfixable only (/live-suite-result has no
|
|
11010
|
+
// entry for a test that passed unchanged), so the original re-run preflight stays off here: a
|
|
11011
|
+
// TRANSIENT_PASS would otherwise be posted as "unfixable". Heal mode keeps the preflight.
|
|
11012
|
+
originalRerun: false,
|
|
10717
11013
|
testVariables: run2.testVariables,
|
|
10718
11014
|
onAICall: healAudit,
|
|
10719
11015
|
modelOverride: run2.healerModel,
|
|
@@ -10724,12 +11020,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10724
11020
|
liveBrowserHandle,
|
|
10725
11021
|
runNativeTest: runNativeWithLiveBrowser
|
|
10726
11022
|
};
|
|
10727
|
-
const suiteHttpFallback = run2.healerFallbackModel && (0,
|
|
11023
|
+
const suiteHttpFallback = run2.healerFallbackModel && (0, import_runner_core9.getAgenticEngine)(run2.healerFallbackModel) === "native" ? run2.healerFallbackModel : null;
|
|
10728
11024
|
let healResult;
|
|
10729
11025
|
if (useGrokHealSuite) {
|
|
10730
11026
|
try {
|
|
10731
11027
|
const grokLogin = await (0, import_fetch_cli_subscription.fetchCliSubscription)({ serverUrl: opts.serverUrl, runId: run2.runId, headers, cli: "grok-build", onLog: log.warn });
|
|
10732
|
-
healResult = await (0,
|
|
11028
|
+
healResult = await (0, import_runner_core9.executeGrokPerTestHeal)(
|
|
10733
11029
|
failedTest.playwrightCode,
|
|
10734
11030
|
failedTest.steps ?? [],
|
|
10735
11031
|
run2.baseUrl,
|
|
@@ -10769,11 +11065,12 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10769
11065
|
});
|
|
10770
11066
|
log.ok(` Fixed: ${failedTest.name}`);
|
|
10771
11067
|
} else {
|
|
10772
|
-
const quotaStopped = (0,
|
|
10773
|
-
const
|
|
11068
|
+
const quotaStopped = (0, import_runner_core7.isHealAiQuotaError)(healResult.error);
|
|
11069
|
+
const timeoutStopped = (0, import_runner_core7.isHealTimeoutError)(healResult.error);
|
|
11070
|
+
const isAppBug = !quotaStopped && !timeoutStopped && (0, import_runner_core7.isAppBugHealError)(healResult.error);
|
|
10774
11071
|
const isConfigIssue = isConfigLikeReason(classifyErrorMessage(healResult.error ?? null));
|
|
10775
11072
|
const status = isAppBug ? "app-bug" : isConfigIssue ? "config-issue" : "unfixable";
|
|
10776
|
-
const diagnosisMsg = isAppBug ? "Application error detected" : isConfigIssue ? "Configuration issue detected" : quotaStopped && healResult.error ? healResult.error : "Could not heal after AI attempts";
|
|
11073
|
+
const diagnosisMsg = isAppBug ? "Application error detected" : isConfigIssue ? "Configuration issue detected" : (quotaStopped || timeoutStopped) && healResult.error ? healResult.error : "Could not heal after AI attempts";
|
|
10777
11074
|
results.push({
|
|
10778
11075
|
testCaseId: failedTest.testCaseId,
|
|
10779
11076
|
status,
|
|
@@ -10781,7 +11078,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10781
11078
|
diagnosis: diagnosisMsg,
|
|
10782
11079
|
duration: Date.now() - testStart
|
|
10783
11080
|
});
|
|
10784
|
-
log[isAppBug || isConfigIssue || quotaStopped ? "warn" : "fail"](` ${isAppBug ? "App bug" : isConfigIssue ? "Config issue" : quotaStopped ? "Not healed (AI provider quota)" : "Could not fix"}: ${failedTest.name}`);
|
|
11081
|
+
log[isAppBug || isConfigIssue || quotaStopped || timeoutStopped ? "warn" : "fail"](` ${isAppBug ? "App bug" : isConfigIssue ? "Config issue" : quotaStopped ? "Not healed (AI provider quota)" : timeoutStopped ? "Not healed (heal time budget)" : "Could not fix"}: ${failedTest.name}`);
|
|
10785
11082
|
}
|
|
10786
11083
|
} catch (err) {
|
|
10787
11084
|
results.push({
|
|
@@ -10794,7 +11091,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10794
11091
|
log.fail(` Heal crashed: ${failedTest.name}`);
|
|
10795
11092
|
}
|
|
10796
11093
|
}
|
|
10797
|
-
(0,
|
|
11094
|
+
(0, import_runner_core9.throwIfRunAborted)();
|
|
10798
11095
|
const lastResult = results[results.length - 1];
|
|
10799
11096
|
if (lastResult) {
|
|
10800
11097
|
fetch(`${opts.serverUrl}/api/runner/runs/${run2.runId}/live-suite-progress`, {
|
|
@@ -10823,7 +11120,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10823
11120
|
auditDrainWarning = drainErr instanceof Error ? drainErr.message : String(drainErr);
|
|
10824
11121
|
console.warn(`[runner] ${auditDrainWarning}`);
|
|
10825
11122
|
}
|
|
10826
|
-
(0,
|
|
11123
|
+
(0, import_runner_core9.throwIfRunAborted)();
|
|
10827
11124
|
const liveRes = await postLiveSuiteResult(opts.serverUrl, headers, run2.runId, {
|
|
10828
11125
|
overall,
|
|
10829
11126
|
duration: totalDuration,
|
|
@@ -10832,7 +11129,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10832
11129
|
`Live suite heal: ${fixedCount} fixed, ${appBugCount} app bugs, ${configIssueCount} config issues, ${unfixableCount} unfixable`,
|
|
10833
11130
|
auditDrainWarning
|
|
10834
11131
|
].filter(Boolean).join("\n")
|
|
10835
|
-
});
|
|
11132
|
+
}, RESULT_POST_BACKOFF_BASE_MS, run2.claimEpoch);
|
|
10836
11133
|
if (!liveRes.ok) {
|
|
10837
11134
|
log.warn(`Failed to post live-suite result: ${liveRes.status}`);
|
|
10838
11135
|
}
|
|
@@ -10914,7 +11211,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10914
11211
|
const res = await fetchRetrying429(`${opts.serverUrl}/api/runner/runs/${run2.runId}/locator-discovery-result`, () => ({
|
|
10915
11212
|
method: "POST",
|
|
10916
11213
|
headers: { ...headers, "Content-Type": "application/json" },
|
|
10917
|
-
|
|
11214
|
+
// The epoch /claim returned: the server refuses a harvest whose run was re-claimed since (stale worker).
|
|
11215
|
+
body: JSON.stringify(withClaimEpoch({
|
|
10918
11216
|
bundles: harvest.bundles,
|
|
10919
11217
|
// A divergence mark is durable (it blocks auto re-harvest and can
|
|
10920
11218
|
// flip the test to needs_manual_authoring), so an environmentally
|
|
@@ -10932,7 +11230,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10932
11230
|
reachedOrder: harvest.reachedOrder,
|
|
10933
11231
|
logs: harvest.logs,
|
|
10934
11232
|
...harvest.environmentalError ? { environmentalError: harvest.environmentalError } : {}
|
|
10935
|
-
}),
|
|
11233
|
+
}, run2.claimEpoch)),
|
|
10936
11234
|
signal: AbortSignal.timeout(3e4)
|
|
10937
11235
|
}));
|
|
10938
11236
|
if (res.ok) {
|
|
@@ -10997,7 +11295,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
10997
11295
|
}
|
|
10998
11296
|
return await res.json();
|
|
10999
11297
|
};
|
|
11000
|
-
ctx.smsReserve = (0,
|
|
11298
|
+
ctx.smsReserve = (0, import_runner_core9.createHttpSmsReserveTransport)({ serverUrl: opts.serverUrl, headers, runId: run2.runId });
|
|
11001
11299
|
}
|
|
11002
11300
|
const canonicalBase = await (0, import_base_url_check.canonicalDiscoveryBaseUrl)(
|
|
11003
11301
|
{ runMode, baseUrl: ctx.baseUrl, native: ctx.medium === "IOS_NATIVE" || ctx.medium === "ANDROID_NATIVE" || run2.framework === "APPIUM" }
|
|
@@ -11059,7 +11357,7 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11059
11357
|
...info.exitReason ? { exitReason: info.exitReason } : {},
|
|
11060
11358
|
// Engine cost, engine id, step, cache + reasoning tokens: same helper as the server's
|
|
11061
11359
|
// Fargate path (execute-run.ts), so the two rows agree.
|
|
11062
|
-
...(0,
|
|
11360
|
+
...(0, import_runner_core9.cliAuditMetadata)(info),
|
|
11063
11361
|
...ledgerAudit ? {
|
|
11064
11362
|
ledger: ledgerAudit.entries,
|
|
11065
11363
|
ledgerEntryCount: ledgerAudit.originalLength,
|
|
@@ -11143,7 +11441,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11143
11441
|
headers: { ...headers, "Content-Type": "application/json" },
|
|
11144
11442
|
// Framework-import runs span MANY features in one Session, so each test
|
|
11145
11443
|
// carries its own suiteName; per-feature deep runs fall back to ctx.featureName.
|
|
11146
|
-
|
|
11444
|
+
// claimEpoch: the server fences each save to this claim (a re-claimed run refuses it with 409).
|
|
11445
|
+
body: JSON.stringify(withClaimEpoch({ ...test, suiteName: test.suiteName ?? ctx.featureName, phase }, run2.claimEpoch)),
|
|
11147
11446
|
signal: AbortSignal.timeout(3e4)
|
|
11148
11447
|
});
|
|
11149
11448
|
if (res.status === 402) throw new import_discovery.PlanLimitReached();
|
|
@@ -11300,7 +11599,8 @@ ${finalVerifyResult.logs ?? ""}`;
|
|
|
11300
11599
|
healthObservations: rest.healthObservations
|
|
11301
11600
|
},
|
|
11302
11601
|
// Only send tests that were NOT confirmed streamed to avoid duplicates
|
|
11303
|
-
tests: rest.tests.filter((t) => !streamedTestKeys.has(fingerprintTest(t)))
|
|
11602
|
+
tests: rest.tests.filter((t) => !streamedTestKeys.has(fingerprintTest(t))),
|
|
11603
|
+
...discoveryRenderTypeFields({ appRenderType: ctx.appRenderType })
|
|
11304
11604
|
}, logs2, { omitScreenshots: isNativeDiscovery });
|
|
11305
11605
|
};
|
|
11306
11606
|
const acquireLoginBrowser = async () => {
|
|
@@ -11602,7 +11902,7 @@ ${line}`)
|
|
|
11602
11902
|
};
|
|
11603
11903
|
} else if (ctx.frameworkImport?.kind === "app-map") {
|
|
11604
11904
|
log.info("Building the app map.");
|
|
11605
|
-
discoveryResult = await (0,
|
|
11905
|
+
discoveryResult = await (0, import_runner_core9.runAppMapOnRunner)(ctx, { onLog: (line) => log.info(line) });
|
|
11606
11906
|
} else if (ctx.frameworkImport?.kind === "repo-enrichment") {
|
|
11607
11907
|
const frameworkImport = ctx.frameworkImport;
|
|
11608
11908
|
const primaryCli = isRepositoryCli(frameworkImport.repoHuntCli) ? frameworkImport.repoHuntCli : "claude-code";
|
|
@@ -11627,7 +11927,7 @@ ${line}`)
|
|
|
11627
11927
|
})
|
|
11628
11928
|
});
|
|
11629
11929
|
} else {
|
|
11630
|
-
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ?
|
|
11930
|
+
const runRepoImport = ctx.frameworkImport?.kind === "repo-analysis" ? import_runner_core9.runRepoAnalysisOnRunner : import_framework_import_runner.runFrameworkImportOnRunner;
|
|
11631
11931
|
discoveryResult = await runRepoImport(ctx, { onTestGenerated, acquireLoginBrowser, onAudit: discoveryAudit }, {
|
|
11632
11932
|
archiveUrl: ctx.frameworkImport?.source === "upload" ? `${opts.serverUrl}/api/runner/runs/${run2.runId}/import-archive` : void 0,
|
|
11633
11933
|
archiveAuthHeader: headers.Authorization,
|
|
@@ -11689,7 +11989,7 @@ ${line}`)
|
|
|
11689
11989
|
}
|
|
11690
11990
|
const resolveCliSubscription = (engine, logLine) => (0, import_fetch_cli_subscription.fetchCliSubscription)({ serverUrl: opts.serverUrl, runId: run2.runId, headers, cli: engine, onLog: logLine });
|
|
11691
11991
|
const claimNonce = claimNonceFor(run2.runId);
|
|
11692
|
-
savedSignIn = ["survey", "deep", "full"].includes(ctx.mode) && claimNonce && typeof run2.claimEpoch === "number" ? await (0,
|
|
11992
|
+
savedSignIn = ["survey", "deep", "full"].includes(ctx.mode) && claimNonce && typeof run2.claimEpoch === "number" ? await (0, import_runner_core9.openSavedSignInSession)((0, import_runner_core9.createSavedSignInClient)({
|
|
11693
11993
|
serverUrl: opts.serverUrl,
|
|
11694
11994
|
runId: run2.runId,
|
|
11695
11995
|
claimEpoch: run2.claimEpoch,
|
|
@@ -11709,7 +12009,7 @@ ${line}`)
|
|
|
11709
12009
|
if (ctx.repoBrief) discoveryResult.repoBrief = ctx.repoBrief;
|
|
11710
12010
|
if (ctx.repoRouteManifest) discoveryResult.repoRouteManifest = ctx.repoRouteManifest;
|
|
11711
12011
|
}
|
|
11712
|
-
discoveryResult = (0,
|
|
12012
|
+
discoveryResult = (0, import_runner_core9.redactDiscoveryCaptureNeedles)(discoveryResult, {
|
|
11713
12013
|
...run2.testVariables,
|
|
11714
12014
|
// The saved sign-in's values, with every one it rotated to during the run.
|
|
11715
12015
|
...savedSignIn?.loaded ? savedSignIn.secrets() : {}
|
|
@@ -11786,7 +12086,7 @@ ${line}`)
|
|
|
11786
12086
|
log.warn("[runner] discovery-har POST failed - final /discovery-result will carry HAR fallback");
|
|
11787
12087
|
}
|
|
11788
12088
|
const finalBody = buildFinalBody(discoveryResult, void 0, discoveryAuditWarning || void 0, auditDroppedCount);
|
|
11789
|
-
const postOk = await postDiscoveryFinalResult(opts.serverUrl, headers, run2.runId, finalBody);
|
|
12089
|
+
const postOk = await postDiscoveryFinalResult(opts.serverUrl, headers, run2.runId, withClaimEpoch(finalBody, run2.claimEpoch));
|
|
11790
12090
|
if (!postOk) throw new Error("Failed to post discovery result (3 attempts, or refused with 413)");
|
|
11791
12091
|
const discoveryError = discoveryResult.error ?? (discoveryResult.summary?.startsWith("Fatal error:") ? discoveryResult.summary : void 0);
|
|
11792
12092
|
if (discoveryError) {
|
|
@@ -11825,14 +12125,15 @@ ${line}`)
|
|
|
11825
12125
|
cliLimit: (0, import_cli_limit.cliLimitFromError)(err),
|
|
11826
12126
|
streamedTestCount: streamedTestKeys.size,
|
|
11827
12127
|
streamedTestCaseIds: Array.from(streamedTestCaseIds),
|
|
11828
|
-
exploreAlreadySaved: explorePosted
|
|
12128
|
+
exploreAlreadySaved: explorePosted,
|
|
12129
|
+
...discoveryRenderTypeFields({ err })
|
|
11829
12130
|
}, `Fatal error: ${msg}${drainMsg}`);
|
|
11830
12131
|
await postShedOn413((body) => fetchRetrying429(`${opts.serverUrl}/api/runner/runs/${run2.runId}/discovery-result`, () => ({
|
|
11831
12132
|
method: "POST",
|
|
11832
12133
|
headers: { ...headers, "Content-Type": "application/json" },
|
|
11833
12134
|
body: JSON.stringify(body),
|
|
11834
12135
|
signal: AbortSignal.timeout(12e4)
|
|
11835
|
-
})), errorBody, "discovery error result", import_result_shed.DISCOVERY_SHED_LADDER).catch(() => {
|
|
12136
|
+
})), withClaimEpoch(errorBody, run2.claimEpoch), "discovery error result", import_result_shed.DISCOVERY_SHED_LADDER).catch(() => {
|
|
11836
12137
|
});
|
|
11837
12138
|
} finally {
|
|
11838
12139
|
if (poolBrowser) browserPool.release(poolBrowser);
|
|
@@ -11866,7 +12167,7 @@ ${line}`)
|
|
|
11866
12167
|
completionTokens: info.completionTokens,
|
|
11867
12168
|
totalTokens: info.totalTokens,
|
|
11868
12169
|
...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
|
|
11869
|
-
metadata: { groupId: info.groupId, iteration: info.iteration, phase: "capture", featureName: ctx.featureName, ...(0,
|
|
12170
|
+
metadata: { groupId: info.groupId, iteration: info.iteration, phase: "capture", featureName: ctx.featureName, ...(0, import_runner_core9.cliAuditMetadata)(info) }
|
|
11870
12171
|
});
|
|
11871
12172
|
};
|
|
11872
12173
|
const logBuffer = [];
|
|
@@ -11965,7 +12266,7 @@ ${line}`)
|
|
|
11965
12266
|
if (mobileSteps.length === 0) {
|
|
11966
12267
|
await postRunResult({
|
|
11967
12268
|
status: "ERROR",
|
|
11968
|
-
error: "No mobile test steps found."
|
|
12269
|
+
error: "CONFIG_ISSUE: No mobile test steps found."
|
|
11969
12270
|
});
|
|
11970
12271
|
log.fail("ERROR - No mobile steps");
|
|
11971
12272
|
return;
|
|
@@ -12041,7 +12342,7 @@ ${line}`)
|
|
|
12041
12342
|
verdict: "fix",
|
|
12042
12343
|
recovered: true,
|
|
12043
12344
|
priorStepsHash: run2.priorStepsHash
|
|
12044
|
-
});
|
|
12345
|
+
}, run2.claimEpoch);
|
|
12045
12346
|
log.ok("\u{1FA7A} Reproduce PASSED \u2014 test recovered (flaky); promotion posted");
|
|
12046
12347
|
} catch (patchErr) {
|
|
12047
12348
|
log.warn(`Failed to post recovered heal result: ${patchErr instanceof Error ? patchErr.message : String(patchErr)}`);
|
|
@@ -12083,7 +12384,7 @@ ${line}`)
|
|
|
12083
12384
|
completionTokens: info.completionTokens,
|
|
12084
12385
|
totalTokens: info.totalTokens,
|
|
12085
12386
|
...hasTurnDetails ? buildAuditTurnPayload(info, run2.testVariables) : { requestBody: { iteration: info.iteration, toolCalls: info.toolCalls } },
|
|
12086
|
-
metadata: { groupId: info.groupId, iteration: info.iteration, testName: run2.testCaseId, platform: "mobile", ...(0,
|
|
12387
|
+
metadata: { groupId: info.groupId, iteration: info.iteration, testName: run2.testCaseId, platform: "mobile", ...(0, import_runner_core9.cliAuditMetadata)(info) }
|
|
12087
12388
|
});
|
|
12088
12389
|
},
|
|
12089
12390
|
runLiveHeal: async (a) => {
|
|
@@ -12278,7 +12579,7 @@ ${line}`)
|
|
|
12278
12579
|
extraTags: heal.extraTags,
|
|
12279
12580
|
removeTags: heal.removeTags,
|
|
12280
12581
|
priorStepsHash: run2.priorStepsHash
|
|
12281
|
-
});
|
|
12582
|
+
}, run2.claimEpoch);
|
|
12282
12583
|
log.ok(`\u{1FA7A} Mobile heal patch posted for test ${run2.testCaseId ?? "(unknown)"} (verdict: ${heal.verdict})`);
|
|
12283
12584
|
} catch (patchErr) {
|
|
12284
12585
|
log.warn(`Failed to post mobile heal patch: ${patchErr instanceof Error ? patchErr.message : String(patchErr)}`);
|
|
@@ -12425,6 +12726,7 @@ ${result2.logs}`);
|
|
|
12425
12726
|
if (storageStatePath) {
|
|
12426
12727
|
log.info(`Using auth state for project ${projectId}`);
|
|
12427
12728
|
}
|
|
12729
|
+
const authSession = (0, import_runner_core8.sharedAuthSessionStamp)(storageStatePath, captureAuth, sharedAuthInjectEnabled());
|
|
12428
12730
|
const runAuthPath = captureAuth ? authPaths?.capturePath : void 0;
|
|
12429
12731
|
const result = await (async () => {
|
|
12430
12732
|
try {
|
|
@@ -12483,49 +12785,25 @@ ${result2.logs}`);
|
|
|
12483
12785
|
runStatus = "ERROR";
|
|
12484
12786
|
log.warn(`\u26A0 CONFIG_ISSUE from native execution - posting ERROR (not FAILED) to skip heal/quarantine`);
|
|
12485
12787
|
}
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
|
|
12494
|
-
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
|
|
12503
|
-
tags: run2.tags ?? [],
|
|
12504
|
-
authStateExpected,
|
|
12505
|
-
authStateInjected: !!storageStatePath,
|
|
12506
|
-
needsStandingHeaders: normalRunAuthPlan.needsStandingHeaders,
|
|
12507
|
-
standingHeadersInjected: !!result.authDelivery?.standingHeadersInjected,
|
|
12508
|
-
error: runError,
|
|
12509
|
-
logs: result.logs,
|
|
12510
|
-
stepResults: result.stepResults,
|
|
12511
|
-
apiCalls: result.apiCalls
|
|
12512
|
-
});
|
|
12513
|
-
if (cold.detected) {
|
|
12514
|
-
runStatus = "ERROR";
|
|
12515
|
-
runError = coldSessionConfigMessage("Authenticated @api test ran", cold.reason, cold.evidence);
|
|
12516
|
-
log.warn(`\u26A0 Cold @api session detected - posting ERROR/CONFIG_ISSUE (not FAILED) to skip heal + avoid a false app_bug`);
|
|
12517
|
-
}
|
|
12518
|
-
}
|
|
12519
|
-
if (runStatus === "PASSED" && Array.isArray(run2.tags) && run2.tags.includes("@api")) {
|
|
12520
|
-
const skipCoverage = detectSkippedApiCoverage(result.stepResults ?? []);
|
|
12521
|
-
if (skipCoverage.detected) {
|
|
12522
|
-
runStatus = "ERROR";
|
|
12523
|
-
runError = skipCoverage.error;
|
|
12524
|
-
log.warn(`\u26A0 ${skipCoverage.logLine}`);
|
|
12525
|
-
}
|
|
12526
|
-
}
|
|
12788
|
+
const authRelabel = (0, import_runner_core8.authReadinessRelabel)({
|
|
12789
|
+
status: runStatus,
|
|
12790
|
+
error: runError,
|
|
12791
|
+
testName: run2.testName ?? run2.testCaseId ?? void 0,
|
|
12792
|
+
tags: run2.tags ?? [],
|
|
12793
|
+
logs: result.logs,
|
|
12794
|
+
stepResults: result.stepResults,
|
|
12795
|
+
apiCalls: result.apiCalls,
|
|
12796
|
+
authStateExpected,
|
|
12797
|
+
authStateInjected: (0, import_runner_core8.storageStateDelivered)(storageStatePath, result),
|
|
12798
|
+
needsStandingHeaders: normalRunAuthPlan.needsStandingHeaders,
|
|
12799
|
+
standingHeadersInjected: !!result.authDelivery?.standingHeadersInjected
|
|
12800
|
+
});
|
|
12801
|
+
if (authRelabel) {
|
|
12802
|
+
runStatus = authRelabel.status;
|
|
12803
|
+
runError = authRelabel.error;
|
|
12804
|
+
log.warn(authRelabel.logLine);
|
|
12527
12805
|
}
|
|
12528
|
-
(0,
|
|
12806
|
+
(0, import_runner_core9.throwIfRunAborted)();
|
|
12529
12807
|
await postRunResult({
|
|
12530
12808
|
status: runStatus,
|
|
12531
12809
|
stepResults: result.stepResults,
|
|
@@ -12542,7 +12820,8 @@ ${result2.logs}`);
|
|
|
12542
12820
|
expectedFailure: result.passed ? result.expectedFailure === true : void 0,
|
|
12543
12821
|
expectedFailureReason: result.passed && result.expectedFailure ? result.expectedFailureReason : void 0,
|
|
12544
12822
|
unexpectedPass: result.passed ? void 0 : result.unexpectedPass === true,
|
|
12545
|
-
bugTestBroken: result.passed ? void 0 : result.bugTestBroken === true
|
|
12823
|
+
bugTestBroken: result.passed ? void 0 : result.bugTestBroken === true,
|
|
12824
|
+
authSession
|
|
12546
12825
|
});
|
|
12547
12826
|
if (result.passed) {
|
|
12548
12827
|
log.ok(`PASSED (${duration}ms)${result.suspiciousPass ? ` \u26A0 suspicious-pass (viewport ${Math.round(result.suspiciousPass.emptyRatio * 100)}% blank)` : ""}`);
|
|
@@ -12558,14 +12837,14 @@ ${result2.logs}`);
|
|
|
12558
12837
|
}
|
|
12559
12838
|
} catch (err) {
|
|
12560
12839
|
if (err instanceof RunNoLongerActiveError) throw err;
|
|
12561
|
-
const abortOutcome = (0,
|
|
12840
|
+
const abortOutcome = (0, import_runner_core9.runAbortOutcome)(err, signal);
|
|
12562
12841
|
if (abortOutcome === "discard") {
|
|
12563
|
-
throw new RunNoLongerActiveError((0,
|
|
12842
|
+
throw new RunNoLongerActiveError((0, import_runner_core9.isSupersededRunAbort)(signal) ? `Run ${run2.runId} was superseded by a newer claim` : `Run ${run2.runId} was cancelled on the server`);
|
|
12564
12843
|
}
|
|
12565
12844
|
if (abortOutcome === "interrupt") {
|
|
12566
12845
|
await postRunResult({
|
|
12567
12846
|
status: "ERROR",
|
|
12568
|
-
error:
|
|
12847
|
+
error: import_runner_core9.RUNNER_SHUTDOWN_INTERRUPT_ERROR,
|
|
12569
12848
|
duration: Date.now() - startTime
|
|
12570
12849
|
});
|
|
12571
12850
|
throw err;
|
|
@@ -12587,6 +12866,7 @@ ${result2.logs}`);
|
|
|
12587
12866
|
}
|
|
12588
12867
|
}
|
|
12589
12868
|
async function startRunner(opts) {
|
|
12869
|
+
(0, import_playwright_signal_handlers.disablePlaywrightSignalHandlers)();
|
|
12590
12870
|
const { serverUrl, authToken, projectId, enableMobile = false, enableMobileTunnel = false } = opts;
|
|
12591
12871
|
const pollIntervalMs = clampPollInterval(opts.pollIntervalMs);
|
|
12592
12872
|
process.env.SERVER_URL = serverUrl;
|
|
@@ -13042,7 +13322,7 @@ async function startRunner(opts) {
|
|
|
13042
13322
|
}
|
|
13043
13323
|
});
|
|
13044
13324
|
};
|
|
13045
|
-
const removeSupersededRunFetchFence = (0,
|
|
13325
|
+
const removeSupersededRunFetchFence = (0, import_runner_core9.installSupersededRunFetchFence)(serverUrl);
|
|
13046
13326
|
const heartbeat = setInterval(sendHeartbeat, 3e4);
|
|
13047
13327
|
sendHeartbeat();
|
|
13048
13328
|
let shuttingDown = false;
|
|
@@ -13054,7 +13334,7 @@ async function startRunner(opts) {
|
|
|
13054
13334
|
headers: { ...headers, "Content-Type": "application/json" },
|
|
13055
13335
|
body: JSON.stringify(withClaimEpoch({
|
|
13056
13336
|
status: "ERROR",
|
|
13057
|
-
error:
|
|
13337
|
+
error: import_runner_core9.RUNNER_SHUTDOWN_INTERRUPT_ERROR
|
|
13058
13338
|
}, claimEpoch)),
|
|
13059
13339
|
signal: AbortSignal.timeout(5e3)
|
|
13060
13340
|
}));
|
|
@@ -13070,7 +13350,7 @@ async function startRunner(opts) {
|
|
|
13070
13350
|
setTimeout(() => {
|
|
13071
13351
|
console.error("Shutdown grace elapsed \u2014 forcing exit");
|
|
13072
13352
|
process.exit(0);
|
|
13073
|
-
}, (0,
|
|
13353
|
+
}, (0, import_runner_core9.shutdownForceExitMs)(SHUTDOWN_FINISH_MS, SHUTDOWN_GRACE_MS)).unref();
|
|
13074
13354
|
clearInterval(heartbeat);
|
|
13075
13355
|
heartbeatKeepalive.stop();
|
|
13076
13356
|
if (liveBrowserHeartbeatTimer) {
|
|
@@ -13088,7 +13368,7 @@ async function startRunner(opts) {
|
|
|
13088
13368
|
if (claimPending) {
|
|
13089
13369
|
await Promise.race([claimPending, new Promise((resolve3) => setTimeout(resolve3, SHUTDOWN_FINISH_MS))]);
|
|
13090
13370
|
}
|
|
13091
|
-
await (0,
|
|
13371
|
+
await (0, import_runner_core9.drainAbortedRuns)({
|
|
13092
13372
|
finishMs: SHUTDOWN_FINISH_MS,
|
|
13093
13373
|
graceMs: SHUTDOWN_GRACE_MS,
|
|
13094
13374
|
inFlight: activeWebRuns,
|
|
@@ -13105,17 +13385,19 @@ async function startRunner(opts) {
|
|
|
13105
13385
|
await stopAppiumServer();
|
|
13106
13386
|
}
|
|
13107
13387
|
await browserPool.shutdown();
|
|
13108
|
-
await (0,
|
|
13388
|
+
await (0, import_runner_core10.closeLocalGoogleBroker)().catch(() => {
|
|
13109
13389
|
});
|
|
13110
13390
|
removeSupersededRunFetchFence();
|
|
13111
13391
|
log.info("Runner stopped.");
|
|
13112
13392
|
process.exit(0);
|
|
13113
13393
|
};
|
|
13114
|
-
process
|
|
13115
|
-
void cleanup();
|
|
13116
|
-
});
|
|
13117
|
-
process.once("SIGTERM", () => {
|
|
13394
|
+
installShutdownSignals(process, () => {
|
|
13118
13395
|
void cleanup();
|
|
13396
|
+
}, {
|
|
13397
|
+
forceExit: () => {
|
|
13398
|
+
console.error("Second interrupt \u2014 forcing exit");
|
|
13399
|
+
process.exit(0);
|
|
13400
|
+
}
|
|
13119
13401
|
});
|
|
13120
13402
|
const PIDS_WATCHDOG_INTERVAL_MS = Math.max(5e3, envInt("RUNNER_PIDS_WATCHDOG_INTERVAL_MS", 3e4));
|
|
13121
13403
|
const PIDS_SOFT_WARN_PCT = Math.max(0, Math.min(100, envInt("RUNNER_PIDS_SOFT_WARN_PCT", 70)));
|
|
@@ -13167,7 +13449,7 @@ async function startRunner(opts) {
|
|
|
13167
13449
|
const abortSupersededRun = (runId, status, serverEpoch) => {
|
|
13168
13450
|
const controller = activeWebRuns.get(runId);
|
|
13169
13451
|
const already = !controller || controller.signal.aborted;
|
|
13170
|
-
if (controller) (0,
|
|
13452
|
+
if (controller) (0, import_runner_core9.abortRun)(controller, "superseded", runId);
|
|
13171
13453
|
activeMobileRuns.get(runId)?.triggerAbort(`was superseded by a newer claim (epoch ${serverEpoch ?? "?"}, status ${status})`);
|
|
13172
13454
|
if (!already) {
|
|
13173
13455
|
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.`);
|
|
@@ -13183,7 +13465,7 @@ async function startRunner(opts) {
|
|
|
13183
13465
|
(runId, status) => {
|
|
13184
13466
|
const controller = activeWebRuns.get(runId);
|
|
13185
13467
|
const already = !controller || controller.signal.aborted;
|
|
13186
|
-
if (controller) (0,
|
|
13468
|
+
if (controller) (0, import_runner_core9.abortRun)(controller, "cancelled", runId);
|
|
13187
13469
|
activeMobileRuns.get(runId)?.triggerAbort(`was cancelled server-side (status ${status})`);
|
|
13188
13470
|
if (!already) log.warn(`Run ${runId} was cancelled server-side (status ${status}) \u2014 stopping it.`);
|
|
13189
13471
|
}
|
|
@@ -13226,6 +13508,10 @@ async function startRunner(opts) {
|
|
|
13226
13508
|
for (const run2 of pending) {
|
|
13227
13509
|
if (shuttingDown) break;
|
|
13228
13510
|
if (activeRuns >= MAX_CONCURRENT) break;
|
|
13511
|
+
if (activeWebRuns.has(run2.runId)) {
|
|
13512
|
+
logDeferredOnce(run2.runId, ` Deferred ${run2.runId} (an earlier claim of it is still settling on this runner)`);
|
|
13513
|
+
continue;
|
|
13514
|
+
}
|
|
13229
13515
|
const runMode = run2.mode ?? "run";
|
|
13230
13516
|
const isMobileOp = enableMobile && isMobileDeviceOp(run2);
|
|
13231
13517
|
const discoveryKind = isMobileOp ? discoveryKindOf(run2.discoveryContext) : null;
|
|
@@ -13437,9 +13723,9 @@ async function startRunner(opts) {
|
|
|
13437
13723
|
runWatchdog.start();
|
|
13438
13724
|
}
|
|
13439
13725
|
activeWebRuns.set(run2.runId, runAbort);
|
|
13440
|
-
const runWritePolicy = (0,
|
|
13441
|
-
const runSignInSecrets = runWritePolicy ? (0,
|
|
13442
|
-
(0,
|
|
13726
|
+
const runWritePolicy = (0, import_runner_core9.parseRunWritePolicy)(run2.runWritePolicy);
|
|
13727
|
+
const runSignInSecrets = runWritePolicy ? (0, import_runner_core9.signInSecretsOf)({ ...run2.discoveryContext?.variableContext?.allTestVariables ?? {}, ...run2.testVariables ?? {} }) : [];
|
|
13728
|
+
(0, import_runner_core9.withRunWritePolicyScope)(runWritePolicy, () => (0, import_runner_core10.withLocalGoogleScope)({ baseUrl: run2.baseUrl, runId: run2.runId }, () => executeRun(run2, opts, headers, requestLiveBrowserHeartbeat, (testCaseId) => {
|
|
13443
13729
|
if (mobilePlatform) {
|
|
13444
13730
|
mobileHealHold.set(testCaseId, { expiry: Date.now() + MOBILE_HEAL_HOLD_MS, platform: mobilePlatform });
|
|
13445
13731
|
}
|
|
@@ -13447,11 +13733,11 @@ async function startRunner(opts) {
|
|
|
13447
13733
|
runsCompleted++;
|
|
13448
13734
|
}).catch((err) => {
|
|
13449
13735
|
const abortReason = runAbort.signal.reason;
|
|
13450
|
-
if ((0,
|
|
13736
|
+
if ((0, import_runner_core9.isRunAbortError)(abortReason) && abortReason.reason === "shutdown") {
|
|
13451
13737
|
log.info(`Run ${run2.runId} was interrupted by runner shutdown.`);
|
|
13452
13738
|
return;
|
|
13453
13739
|
}
|
|
13454
|
-
if ((0,
|
|
13740
|
+
if ((0, import_runner_core9.isSupersededRunAbort)(runAbort.signal)) {
|
|
13455
13741
|
log.info(`Run ${run2.runId} was superseded by a newer claim; this runner posted nothing more for it.`);
|
|
13456
13742
|
return;
|
|
13457
13743
|
}
|
|
@@ -13489,7 +13775,7 @@ async function startRunner(opts) {
|
|
|
13489
13775
|
const action = authRefusalAction({
|
|
13490
13776
|
consecutive: consecutiveAuthRefusals,
|
|
13491
13777
|
activeRuns,
|
|
13492
|
-
cloud:
|
|
13778
|
+
cloud: IS_CLOUD
|
|
13493
13779
|
});
|
|
13494
13780
|
if (action === "exit") {
|
|
13495
13781
|
log.fail(`${message} (the server refused it ${consecutiveAuthRefusals} times in a row)`);
|
|
@@ -13545,14 +13831,15 @@ async function checkRunnerLogin(serverUrl, token, fetchImpl = fetch) {
|
|
|
13545
13831
|
|
|
13546
13832
|
// src/services/sign-in-capture.ts
|
|
13547
13833
|
var import_browser_config3 = __toESM(require_browser_config());
|
|
13834
|
+
var import_google_local_signin = __toESM(require_google_local_signin());
|
|
13548
13835
|
import { chromium } from "playwright";
|
|
13549
13836
|
async function captureSignInInteractively(url, ready, opts = {}) {
|
|
13550
13837
|
const target = new URL(url);
|
|
13551
13838
|
let browser;
|
|
13552
13839
|
try {
|
|
13553
|
-
browser = await chromium.launch({ headless: false, ignoreDefaultArgs: ["--enable-automation"], channel: "chrome", env: (0, import_browser_config3.browserLaunchEnv)() });
|
|
13840
|
+
browser = await chromium.launch({ headless: false, ignoreDefaultArgs: ["--enable-automation"], args: [...import_google_local_signin.SIGN_IN_WINDOW_ARGS], channel: "chrome", env: (0, import_browser_config3.browserLaunchEnv)() });
|
|
13554
13841
|
} catch {
|
|
13555
|
-
browser = await chromium.launch({ headless: false, ignoreDefaultArgs: ["--enable-automation"], env: (0, import_browser_config3.browserLaunchEnv)() });
|
|
13842
|
+
browser = await chromium.launch({ headless: false, ignoreDefaultArgs: ["--enable-automation"], args: [...import_google_local_signin.SIGN_IN_WINDOW_ARGS], env: (0, import_browser_config3.browserLaunchEnv)() });
|
|
13556
13843
|
}
|
|
13557
13844
|
return captureFromBrowser(browser, target, ready, opts.timeoutMs ?? SIGN_IN_TIMEOUT_MS);
|
|
13558
13845
|
}
|
|
@@ -13813,7 +14100,9 @@ async function superviseRunnerStart(argv, authTokenForChild) {
|
|
|
13813
14100
|
stdio: "inherit",
|
|
13814
14101
|
env: childEnv
|
|
13815
14102
|
});
|
|
14103
|
+
let signalled = false;
|
|
13816
14104
|
const onSignal = (sig) => {
|
|
14105
|
+
signalled = true;
|
|
13817
14106
|
try {
|
|
13818
14107
|
child.kill(sig);
|
|
13819
14108
|
} catch {
|
|
@@ -13825,19 +14114,19 @@ async function superviseRunnerStart(argv, authTokenForChild) {
|
|
|
13825
14114
|
process.removeListener("SIGINT", onSignal);
|
|
13826
14115
|
process.removeListener("SIGTERM", onSignal);
|
|
13827
14116
|
if (signal === "SIGINT" || signal === "SIGTERM") {
|
|
13828
|
-
resolve3(0);
|
|
14117
|
+
resolve3({ code: 0, signalled: true });
|
|
13829
14118
|
return;
|
|
13830
14119
|
}
|
|
13831
|
-
resolve3(code ?? 1);
|
|
14120
|
+
resolve3({ code: code ?? 1, signalled });
|
|
13832
14121
|
});
|
|
13833
14122
|
});
|
|
13834
14123
|
console.log("\u{1F6E1} Runner supervisor enabled \u2014 will auto-restart on crash/stuck exit");
|
|
13835
14124
|
console.log(" (disable with --no-supervise or VALIDATE_RUNNER_SUPERVISE=0)");
|
|
13836
14125
|
for (; ; ) {
|
|
13837
14126
|
const startedAt = Date.now();
|
|
13838
|
-
const code = await runOnce();
|
|
14127
|
+
const { code, signalled } = await runOnce();
|
|
13839
14128
|
const next = nextSupervisorAction(
|
|
13840
|
-
{ code, ranMs: Date.now() - startedAt, now: Date.now() },
|
|
14129
|
+
{ code, ranMs: Date.now() - startedAt, now: Date.now(), signalled },
|
|
13841
14130
|
state2,
|
|
13842
14131
|
{ maxRestartsPerHour, minBackoffMs, maxBackoffMs }
|
|
13843
14132
|
);
|
|
@@ -13918,7 +14207,7 @@ program.command("start").description("Start the local test runner").option("--to
|
|
|
13918
14207
|
console.warn("\u26A0 --tunnel has no effect without --mobile (the mobile bridge it exposes only starts with --mobile). Ignoring --tunnel.");
|
|
13919
14208
|
}
|
|
13920
14209
|
{
|
|
13921
|
-
const { hasLocalGoogleLogin, GOOGLE_LOGIN_COMMAND } = await import("./dist-
|
|
14210
|
+
const { hasLocalGoogleLogin, GOOGLE_LOGIN_COMMAND } = await import("./dist-52M5RDZ6.mjs");
|
|
13922
14211
|
if (process.stdout.isTTY && !await hasLocalGoogleLogin()) {
|
|
13923
14212
|
console.log(`Tip: for apps with "Continue with Google", sign in to Google once on this machine: ${GOOGLE_LOGIN_COMMAND}`);
|
|
13924
14213
|
}
|
|
@@ -13948,7 +14237,7 @@ async function askLine(question) {
|
|
|
13948
14237
|
}
|
|
13949
14238
|
}
|
|
13950
14239
|
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) => {
|
|
13951
|
-
const { signInLocalGoogle, LOCAL_GOOGLE_WARNING, grantedSite, defaultStoreKind } = await import("./dist-
|
|
14240
|
+
const { signInLocalGoogle, LOCAL_GOOGLE_WARNING, grantedSite, defaultStoreKind } = await import("./dist-52M5RDZ6.mjs");
|
|
13952
14241
|
console.log(LOCAL_GOOGLE_WARNING);
|
|
13953
14242
|
if (!opts.yes) {
|
|
13954
14243
|
const answer = await askLine('Is this a dedicated test Google account? Type "yes" to continue: ');
|
|
@@ -13983,7 +14272,7 @@ program.command("google-login").description('Sign in to a TEST Google account on
|
|
|
13983
14272
|
}
|
|
13984
14273
|
});
|
|
13985
14274
|
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) => {
|
|
13986
|
-
const { allowLocalGoogleSites, grantedSite } = await import("./dist-
|
|
14275
|
+
const { allowLocalGoogleSites, grantedSite } = await import("./dist-52M5RDZ6.mjs");
|
|
13987
14276
|
const site = grantedSite(url);
|
|
13988
14277
|
if (!site) {
|
|
13989
14278
|
console.error("\u2717 Not a web address: give it like https://app.example.com");
|
|
@@ -13997,7 +14286,7 @@ program.command("google-allow-site <url>").description("Let the Google sign-in o
|
|
|
13997
14286
|
console.log(`\u2713 Tests on this machine may now use the Google sign-in for: ${sites.join(", ")}`);
|
|
13998
14287
|
});
|
|
13999
14288
|
program.command("google-logout").description("Forget the Google sign-in saved on this machine").action(async () => {
|
|
14000
|
-
const { deleteLocalGoogleLogin } = await import("./dist-
|
|
14289
|
+
const { deleteLocalGoogleLogin } = await import("./dist-52M5RDZ6.mjs");
|
|
14001
14290
|
await deleteLocalGoogleLogin();
|
|
14002
14291
|
console.log("\u2713 Google sign-in removed from this machine");
|
|
14003
14292
|
});
|