@hasna/testers 0.0.6 → 0.0.8
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/dist/cli/index.js +1424 -395
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/environments.d.ts +22 -0
- package/dist/db/environments.d.ts.map +1 -0
- package/dist/db/runs.d.ts.map +1 -1
- package/dist/db/scenarios.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +422 -171
- package/dist/lib/assertions.d.ts +26 -0
- package/dist/lib/assertions.d.ts.map +1 -0
- package/dist/lib/browser-lightpanda.d.ts +43 -0
- package/dist/lib/browser-lightpanda.d.ts.map +1 -0
- package/dist/lib/browser.d.ts +9 -3
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/ci.d.ts +2 -0
- package/dist/lib/ci.d.ts.map +1 -0
- package/dist/lib/openapi-import.d.ts +8 -0
- package/dist/lib/openapi-import.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +24 -0
- package/dist/lib/recorder.d.ts.map +1 -0
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/visual-diff.d.ts +36 -0
- package/dist/lib/visual-diff.d.ts.map +1 -0
- package/dist/mcp/index.js +341 -12
- package/dist/server/index.js +335 -12
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -11,6 +11,7 @@ var __export = (target, all) => {
|
|
|
11
11
|
});
|
|
12
12
|
};
|
|
13
13
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
14
|
+
var __require = import.meta.require;
|
|
14
15
|
|
|
15
16
|
// src/types/index.ts
|
|
16
17
|
function projectFromRow(row) {
|
|
@@ -50,6 +51,7 @@ function scenarioFromRow(row) {
|
|
|
50
51
|
requiresAuth: row.requires_auth === 1,
|
|
51
52
|
authConfig: row.auth_config ? JSON.parse(row.auth_config) : null,
|
|
52
53
|
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
54
|
+
assertions: JSON.parse(row.assertions || "[]"),
|
|
53
55
|
version: row.version,
|
|
54
56
|
createdAt: row.created_at,
|
|
55
57
|
updatedAt: row.updated_at
|
|
@@ -69,7 +71,8 @@ function runFromRow(row) {
|
|
|
69
71
|
failed: row.failed,
|
|
70
72
|
startedAt: row.started_at,
|
|
71
73
|
finishedAt: row.finished_at,
|
|
72
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
|
74
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
75
|
+
isBaseline: row.is_baseline === 1
|
|
73
76
|
};
|
|
74
77
|
}
|
|
75
78
|
function resultFromRow(row) {
|
|
@@ -424,10 +427,190 @@ var init_database = __esm(() => {
|
|
|
424
427
|
CREATE INDEX IF NOT EXISTS idx_deps_scenario ON scenario_dependencies(scenario_id);
|
|
425
428
|
CREATE INDEX IF NOT EXISTS idx_deps_depends ON scenario_dependencies(depends_on);
|
|
426
429
|
CREATE INDEX IF NOT EXISTS idx_flows_project ON flows(project_id);
|
|
430
|
+
`,
|
|
431
|
+
`
|
|
432
|
+
ALTER TABLE scenarios ADD COLUMN assertions TEXT DEFAULT '[]';
|
|
433
|
+
`,
|
|
434
|
+
`
|
|
435
|
+
CREATE TABLE IF NOT EXISTS environments (
|
|
436
|
+
id TEXT PRIMARY KEY,
|
|
437
|
+
name TEXT NOT NULL UNIQUE,
|
|
438
|
+
url TEXT NOT NULL,
|
|
439
|
+
auth_preset_name TEXT,
|
|
440
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
441
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
442
|
+
metadata TEXT DEFAULT '{}',
|
|
443
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
444
|
+
);
|
|
445
|
+
`,
|
|
446
|
+
`
|
|
447
|
+
ALTER TABLE runs ADD COLUMN is_baseline INTEGER NOT NULL DEFAULT 0;
|
|
427
448
|
`
|
|
428
449
|
];
|
|
429
450
|
});
|
|
430
451
|
|
|
452
|
+
// src/lib/browser-lightpanda.ts
|
|
453
|
+
var exports_browser_lightpanda = {};
|
|
454
|
+
__export(exports_browser_lightpanda, {
|
|
455
|
+
startLightpandaServer: () => startLightpandaServer,
|
|
456
|
+
launchLightpanda: () => launchLightpanda,
|
|
457
|
+
isLightpandaAvailable: () => isLightpandaAvailable,
|
|
458
|
+
installLightpanda: () => installLightpanda,
|
|
459
|
+
getLightpandaPage: () => getLightpandaPage,
|
|
460
|
+
closeLightpanda: () => closeLightpanda
|
|
461
|
+
});
|
|
462
|
+
import { chromium } from "playwright";
|
|
463
|
+
import { spawn } from "child_process";
|
|
464
|
+
function isLightpandaAvailable() {
|
|
465
|
+
try {
|
|
466
|
+
const possiblePaths = [
|
|
467
|
+
`${process.env["HOME"]}/.cache/lightpanda-node/lightpanda`,
|
|
468
|
+
process.env["LIGHTPANDA_EXECUTABLE_PATH"]
|
|
469
|
+
];
|
|
470
|
+
for (const p of possiblePaths) {
|
|
471
|
+
if (p) {
|
|
472
|
+
try {
|
|
473
|
+
const { existsSync: existsSync2 } = __require("fs");
|
|
474
|
+
if (existsSync2(p))
|
|
475
|
+
return true;
|
|
476
|
+
} catch {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const { execSync } = __require("child_process");
|
|
482
|
+
execSync("lightpanda --version", { stdio: "ignore", timeout: 5000 });
|
|
483
|
+
return true;
|
|
484
|
+
} catch {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function findLightpandaBinary() {
|
|
489
|
+
const envPath = process.env["LIGHTPANDA_EXECUTABLE_PATH"];
|
|
490
|
+
if (envPath)
|
|
491
|
+
return envPath;
|
|
492
|
+
const cachePath = `${process.env["HOME"]}/.cache/lightpanda-node/lightpanda`;
|
|
493
|
+
try {
|
|
494
|
+
const { existsSync: existsSync2 } = __require("fs");
|
|
495
|
+
if (existsSync2(cachePath))
|
|
496
|
+
return cachePath;
|
|
497
|
+
} catch {}
|
|
498
|
+
return "lightpanda";
|
|
499
|
+
}
|
|
500
|
+
async function startLightpandaServer(port) {
|
|
501
|
+
const binary = findLightpandaBinary();
|
|
502
|
+
const cdpPort = port ?? 9222 + Math.floor(Math.random() * 1000);
|
|
503
|
+
return new Promise((resolve, reject) => {
|
|
504
|
+
const proc = spawn(binary, ["serve", "--port", String(cdpPort)], {
|
|
505
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
506
|
+
});
|
|
507
|
+
let resolved = false;
|
|
508
|
+
const timeout = setTimeout(() => {
|
|
509
|
+
if (!resolved) {
|
|
510
|
+
resolved = true;
|
|
511
|
+
resolve({
|
|
512
|
+
process: proc,
|
|
513
|
+
wsEndpoint: `ws://127.0.0.1:${cdpPort}`
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}, 5000);
|
|
517
|
+
proc.stdout?.on("data", (data) => {
|
|
518
|
+
const output = data.toString();
|
|
519
|
+
if (output.includes("127.0.0.1") || output.includes("listening") || output.includes("DevTools")) {
|
|
520
|
+
if (!resolved) {
|
|
521
|
+
resolved = true;
|
|
522
|
+
clearTimeout(timeout);
|
|
523
|
+
resolve({
|
|
524
|
+
process: proc,
|
|
525
|
+
wsEndpoint: `ws://127.0.0.1:${cdpPort}`
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
proc.stderr?.on("data", (data) => {
|
|
531
|
+
const output = data.toString();
|
|
532
|
+
if (output.includes("127.0.0.1") || output.includes("listening")) {
|
|
533
|
+
if (!resolved) {
|
|
534
|
+
resolved = true;
|
|
535
|
+
clearTimeout(timeout);
|
|
536
|
+
resolve({
|
|
537
|
+
process: proc,
|
|
538
|
+
wsEndpoint: `ws://127.0.0.1:${cdpPort}`
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
proc.on("error", (err) => {
|
|
544
|
+
clearTimeout(timeout);
|
|
545
|
+
if (!resolved) {
|
|
546
|
+
resolved = true;
|
|
547
|
+
reject(new BrowserError(`Failed to start Lightpanda: ${err.message}. ` + `Install it with: bun install @lightpanda/browser`));
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
proc.on("exit", (code) => {
|
|
551
|
+
if (!resolved) {
|
|
552
|
+
resolved = true;
|
|
553
|
+
clearTimeout(timeout);
|
|
554
|
+
reject(new BrowserError(`Lightpanda exited with code ${code}. ` + `Install it with: bun install @lightpanda/browser`));
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
async function launchLightpanda(_options) {
|
|
560
|
+
try {
|
|
561
|
+
const { process: proc, wsEndpoint } = await startLightpandaServer();
|
|
562
|
+
lightpandaProcess = proc;
|
|
563
|
+
const browser = await chromium.connectOverCDP(wsEndpoint);
|
|
564
|
+
return browser;
|
|
565
|
+
} catch (error) {
|
|
566
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
567
|
+
throw new BrowserError(`Failed to launch Lightpanda: ${message}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
async function getLightpandaPage(browser, options) {
|
|
571
|
+
try {
|
|
572
|
+
const contexts = browser.contexts();
|
|
573
|
+
const context = contexts.length > 0 ? contexts[0] : await browser.newContext({
|
|
574
|
+
viewport: options?.viewport ?? { width: 1280, height: 720 },
|
|
575
|
+
userAgent: options?.userAgent,
|
|
576
|
+
locale: options?.locale
|
|
577
|
+
});
|
|
578
|
+
const page = await context.newPage();
|
|
579
|
+
return page;
|
|
580
|
+
} catch (error) {
|
|
581
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
582
|
+
throw new BrowserError(`Failed to create Lightpanda page: ${message}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function closeLightpanda(browser) {
|
|
586
|
+
try {
|
|
587
|
+
await browser.close();
|
|
588
|
+
} catch {}
|
|
589
|
+
if (lightpandaProcess) {
|
|
590
|
+
try {
|
|
591
|
+
lightpandaProcess.kill("SIGTERM");
|
|
592
|
+
lightpandaProcess = null;
|
|
593
|
+
} catch {}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async function installLightpanda() {
|
|
597
|
+
const { execSync } = __require("child_process");
|
|
598
|
+
try {
|
|
599
|
+
execSync("bun install @lightpanda/browser", {
|
|
600
|
+
stdio: "inherit",
|
|
601
|
+
cwd: process.env["HOME"]
|
|
602
|
+
});
|
|
603
|
+
} catch (error) {
|
|
604
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
605
|
+
throw new BrowserError(`Failed to install Lightpanda: ${message}
|
|
606
|
+
` + `Try manually: bun install @lightpanda/browser`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
var lightpandaProcess = null;
|
|
610
|
+
var init_browser_lightpanda = __esm(() => {
|
|
611
|
+
init_types();
|
|
612
|
+
});
|
|
613
|
+
|
|
431
614
|
// src/db/flows.ts
|
|
432
615
|
var exports_flows = {};
|
|
433
616
|
__export(exports_flows, {
|
|
@@ -4577,9 +4760,9 @@ function createScenario(input) {
|
|
|
4577
4760
|
const short_id = nextShortId(input.projectId);
|
|
4578
4761
|
const timestamp = now();
|
|
4579
4762
|
db2.query(`
|
|
4580
|
-
INSERT INTO scenarios (id, short_id, project_id, name, description, steps, tags, priority, model, timeout_ms, target_path, requires_auth, auth_config, metadata, version, created_at, updated_at)
|
|
4581
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
4582
|
-
`).run(id, short_id, input.projectId ?? null, input.name, input.description, JSON.stringify(input.steps ?? []), JSON.stringify(input.tags ?? []), input.priority ?? "medium", input.model ?? null, input.timeoutMs ?? null, input.targetPath ?? null, input.requiresAuth ? 1 : 0, input.authConfig ? JSON.stringify(input.authConfig) : null, input.metadata ? JSON.stringify(input.metadata) : null, timestamp, timestamp);
|
|
4763
|
+
INSERT INTO scenarios (id, short_id, project_id, name, description, steps, tags, priority, model, timeout_ms, target_path, requires_auth, auth_config, metadata, assertions, version, created_at, updated_at)
|
|
4764
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
|
4765
|
+
`).run(id, short_id, input.projectId ?? null, input.name, input.description, JSON.stringify(input.steps ?? []), JSON.stringify(input.tags ?? []), input.priority ?? "medium", input.model ?? null, input.timeoutMs ?? null, input.targetPath ?? null, input.requiresAuth ? 1 : 0, input.authConfig ? JSON.stringify(input.authConfig) : null, input.metadata ? JSON.stringify(input.metadata) : null, JSON.stringify(input.assertions ?? []), timestamp, timestamp);
|
|
4583
4766
|
return getScenario(id);
|
|
4584
4767
|
}
|
|
4585
4768
|
function getScenario(id) {
|
|
@@ -4697,6 +4880,10 @@ function updateScenario(id, input, version) {
|
|
|
4697
4880
|
sets.push("metadata = ?");
|
|
4698
4881
|
params.push(JSON.stringify(input.metadata));
|
|
4699
4882
|
}
|
|
4883
|
+
if (input.assertions !== undefined) {
|
|
4884
|
+
sets.push("assertions = ?");
|
|
4885
|
+
params.push(JSON.stringify(input.assertions));
|
|
4886
|
+
}
|
|
4700
4887
|
if (sets.length === 0) {
|
|
4701
4888
|
return existing;
|
|
4702
4889
|
}
|
|
@@ -4827,6 +5014,10 @@ function updateRun(id, updates) {
|
|
|
4827
5014
|
sets.push("metadata = ?");
|
|
4828
5015
|
params.push(updates.metadata);
|
|
4829
5016
|
}
|
|
5017
|
+
if (updates.is_baseline !== undefined) {
|
|
5018
|
+
sets.push("is_baseline = ?");
|
|
5019
|
+
params.push(updates.is_baseline);
|
|
5020
|
+
}
|
|
4830
5021
|
if (sets.length === 0) {
|
|
4831
5022
|
return existing;
|
|
4832
5023
|
}
|
|
@@ -4998,14 +5189,22 @@ function listAgents() {
|
|
|
4998
5189
|
}
|
|
4999
5190
|
|
|
5000
5191
|
// src/lib/browser.ts
|
|
5001
|
-
import { chromium } from "playwright";
|
|
5192
|
+
import { chromium as chromium2 } from "playwright";
|
|
5002
5193
|
init_types();
|
|
5003
5194
|
var DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
5004
5195
|
async function launchBrowser(options) {
|
|
5196
|
+
const engine = options?.engine ?? process.env["TESTERS_BROWSER_ENGINE"] ?? "playwright";
|
|
5197
|
+
if (engine === "lightpanda") {
|
|
5198
|
+
const { launchLightpanda: launchLightpanda2, isLightpandaAvailable: isLightpandaAvailable2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
5199
|
+
if (!isLightpandaAvailable2()) {
|
|
5200
|
+
throw new BrowserError("Lightpanda not installed. Run: testers install-browser --engine lightpanda");
|
|
5201
|
+
}
|
|
5202
|
+
return launchLightpanda2({ viewport: options?.viewport });
|
|
5203
|
+
}
|
|
5005
5204
|
const headless = options?.headless ?? true;
|
|
5006
5205
|
const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
|
|
5007
5206
|
try {
|
|
5008
|
-
const browser = await
|
|
5207
|
+
const browser = await chromium2.launch({
|
|
5009
5208
|
headless,
|
|
5010
5209
|
args: [
|
|
5011
5210
|
`--window-size=${viewport.width},${viewport.height}`
|
|
@@ -5018,6 +5217,11 @@ async function launchBrowser(options) {
|
|
|
5018
5217
|
}
|
|
5019
5218
|
}
|
|
5020
5219
|
async function getPage(browser, options) {
|
|
5220
|
+
const engine = options?.engine ?? "playwright";
|
|
5221
|
+
if (engine === "lightpanda") {
|
|
5222
|
+
const { getLightpandaPage: getLightpandaPage2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
5223
|
+
return getLightpandaPage2(browser, options);
|
|
5224
|
+
}
|
|
5021
5225
|
const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
|
|
5022
5226
|
try {
|
|
5023
5227
|
const context = await browser.newContext({
|
|
@@ -5032,7 +5236,11 @@ async function getPage(browser, options) {
|
|
|
5032
5236
|
throw new BrowserError(`Failed to create page: ${message}`);
|
|
5033
5237
|
}
|
|
5034
5238
|
}
|
|
5035
|
-
async function closeBrowser(browser) {
|
|
5239
|
+
async function closeBrowser(browser, engine) {
|
|
5240
|
+
if (engine === "lightpanda") {
|
|
5241
|
+
const { closeLightpanda: closeLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
|
|
5242
|
+
return closeLightpanda2(browser);
|
|
5243
|
+
}
|
|
5036
5244
|
try {
|
|
5037
5245
|
await browser.close();
|
|
5038
5246
|
} catch (error) {
|
|
@@ -5966,12 +6174,124 @@ function loadConfig() {
|
|
|
5966
6174
|
return config;
|
|
5967
6175
|
}
|
|
5968
6176
|
|
|
6177
|
+
// src/lib/webhooks.ts
|
|
6178
|
+
init_database();
|
|
6179
|
+
function fromRow(row) {
|
|
6180
|
+
return {
|
|
6181
|
+
id: row.id,
|
|
6182
|
+
url: row.url,
|
|
6183
|
+
events: JSON.parse(row.events),
|
|
6184
|
+
projectId: row.project_id,
|
|
6185
|
+
secret: row.secret,
|
|
6186
|
+
active: row.active === 1,
|
|
6187
|
+
createdAt: row.created_at
|
|
6188
|
+
};
|
|
6189
|
+
}
|
|
6190
|
+
function listWebhooks(projectId) {
|
|
6191
|
+
const db2 = getDatabase();
|
|
6192
|
+
let query = "SELECT * FROM webhooks WHERE active = 1";
|
|
6193
|
+
const params = [];
|
|
6194
|
+
if (projectId) {
|
|
6195
|
+
query += " AND (project_id = ? OR project_id IS NULL)";
|
|
6196
|
+
params.push(projectId);
|
|
6197
|
+
}
|
|
6198
|
+
query += " ORDER BY created_at DESC";
|
|
6199
|
+
const rows = db2.query(query).all(...params);
|
|
6200
|
+
return rows.map(fromRow);
|
|
6201
|
+
}
|
|
6202
|
+
function signPayload(body, secret) {
|
|
6203
|
+
const encoder = new TextEncoder;
|
|
6204
|
+
const key = encoder.encode(secret);
|
|
6205
|
+
const data = encoder.encode(body);
|
|
6206
|
+
let hash = 0;
|
|
6207
|
+
for (let i = 0;i < data.length; i++) {
|
|
6208
|
+
hash = (hash << 5) - hash + data[i] + (key[i % key.length] ?? 0) | 0;
|
|
6209
|
+
}
|
|
6210
|
+
return `sha256=${Math.abs(hash).toString(16).padStart(16, "0")}`;
|
|
6211
|
+
}
|
|
6212
|
+
function formatSlackPayload(payload) {
|
|
6213
|
+
const status = payload.run.status === "passed" ? ":white_check_mark:" : ":x:";
|
|
6214
|
+
const color = payload.run.status === "passed" ? "#22c55e" : "#ef4444";
|
|
6215
|
+
return {
|
|
6216
|
+
attachments: [
|
|
6217
|
+
{
|
|
6218
|
+
color,
|
|
6219
|
+
blocks: [
|
|
6220
|
+
{
|
|
6221
|
+
type: "section",
|
|
6222
|
+
text: {
|
|
6223
|
+
type: "mrkdwn",
|
|
6224
|
+
text: `${status} *Test Run ${payload.run.status.toUpperCase()}*
|
|
6225
|
+
` + `URL: ${payload.run.url}
|
|
6226
|
+
` + `Results: ${payload.run.passed}/${payload.run.total} passed` + (payload.run.failed > 0 ? ` (${payload.run.failed} failed)` : "") + (payload.schedule ? `
|
|
6227
|
+
Schedule: ${payload.schedule.name}` : "")
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
]
|
|
6231
|
+
}
|
|
6232
|
+
]
|
|
6233
|
+
};
|
|
6234
|
+
}
|
|
6235
|
+
async function dispatchWebhooks(event, run, schedule) {
|
|
6236
|
+
const webhooks = listWebhooks(run.projectId ?? undefined);
|
|
6237
|
+
const payload = {
|
|
6238
|
+
event,
|
|
6239
|
+
run: {
|
|
6240
|
+
id: run.id,
|
|
6241
|
+
url: run.url,
|
|
6242
|
+
status: run.status,
|
|
6243
|
+
passed: run.passed,
|
|
6244
|
+
failed: run.failed,
|
|
6245
|
+
total: run.total
|
|
6246
|
+
},
|
|
6247
|
+
schedule,
|
|
6248
|
+
timestamp: new Date().toISOString()
|
|
6249
|
+
};
|
|
6250
|
+
for (const webhook of webhooks) {
|
|
6251
|
+
if (!webhook.events.includes(event) && !webhook.events.includes("*"))
|
|
6252
|
+
continue;
|
|
6253
|
+
const isSlack = webhook.url.includes("hooks.slack.com");
|
|
6254
|
+
const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : JSON.stringify(payload);
|
|
6255
|
+
const headers = {
|
|
6256
|
+
"Content-Type": "application/json"
|
|
6257
|
+
};
|
|
6258
|
+
if (webhook.secret) {
|
|
6259
|
+
headers["X-Testers-Signature"] = signPayload(body, webhook.secret);
|
|
6260
|
+
}
|
|
6261
|
+
try {
|
|
6262
|
+
const response = await fetch(webhook.url, {
|
|
6263
|
+
method: "POST",
|
|
6264
|
+
headers,
|
|
6265
|
+
body
|
|
6266
|
+
});
|
|
6267
|
+
if (!response.ok) {
|
|
6268
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
6269
|
+
await fetch(webhook.url, { method: "POST", headers, body });
|
|
6270
|
+
}
|
|
6271
|
+
} catch {}
|
|
6272
|
+
}
|
|
6273
|
+
}
|
|
6274
|
+
|
|
5969
6275
|
// src/lib/runner.ts
|
|
5970
6276
|
var eventHandler = null;
|
|
5971
6277
|
function emit(event) {
|
|
5972
6278
|
if (eventHandler)
|
|
5973
6279
|
eventHandler(event);
|
|
5974
6280
|
}
|
|
6281
|
+
function withTimeout(promise, ms, label) {
|
|
6282
|
+
return new Promise((resolve, reject) => {
|
|
6283
|
+
const timer = setTimeout(() => {
|
|
6284
|
+
reject(new Error(`Scenario timeout after ${ms}ms: ${label}`));
|
|
6285
|
+
}, ms);
|
|
6286
|
+
promise.then((val) => {
|
|
6287
|
+
clearTimeout(timer);
|
|
6288
|
+
resolve(val);
|
|
6289
|
+
}, (err) => {
|
|
6290
|
+
clearTimeout(timer);
|
|
6291
|
+
reject(err);
|
|
6292
|
+
});
|
|
6293
|
+
});
|
|
6294
|
+
}
|
|
5975
6295
|
async function runSingleScenario(scenario, runId, options) {
|
|
5976
6296
|
const config = loadConfig();
|
|
5977
6297
|
const model = resolveModel(options.model ?? scenario.model ?? config.defaultModel);
|
|
@@ -5989,13 +6309,14 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
5989
6309
|
let browser = null;
|
|
5990
6310
|
let page = null;
|
|
5991
6311
|
try {
|
|
5992
|
-
browser = await launchBrowser({ headless: !(options.headed ?? false) });
|
|
6312
|
+
browser = await launchBrowser({ headless: !(options.headed ?? false), engine: options.engine });
|
|
5993
6313
|
page = await getPage(browser, {
|
|
5994
6314
|
viewport: config.browser.viewport
|
|
5995
6315
|
});
|
|
5996
6316
|
const targetUrl = scenario.targetPath ? `${options.url.replace(/\/$/, "")}${scenario.targetPath}` : options.url;
|
|
5997
|
-
|
|
5998
|
-
|
|
6317
|
+
const scenarioTimeout = scenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
|
|
6318
|
+
await page.goto(targetUrl, { timeout: Math.min(scenarioTimeout, 30000) });
|
|
6319
|
+
const agentResult = await withTimeout(runAgentLoop({
|
|
5999
6320
|
client,
|
|
6000
6321
|
page,
|
|
6001
6322
|
scenario,
|
|
@@ -6016,7 +6337,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
6016
6337
|
stepNumber: stepEvent.stepNumber
|
|
6017
6338
|
});
|
|
6018
6339
|
}
|
|
6019
|
-
});
|
|
6340
|
+
}), scenarioTimeout, scenario.name);
|
|
6020
6341
|
for (const ss of agentResult.screenshots) {
|
|
6021
6342
|
createScreenshot({
|
|
6022
6343
|
resultId: result.id,
|
|
@@ -6053,7 +6374,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
6053
6374
|
return updatedResult;
|
|
6054
6375
|
} finally {
|
|
6055
6376
|
if (browser)
|
|
6056
|
-
await closeBrowser(browser);
|
|
6377
|
+
await closeBrowser(browser, options.engine);
|
|
6057
6378
|
}
|
|
6058
6379
|
}
|
|
6059
6380
|
async function runBatch(scenarios, options) {
|
|
@@ -6148,6 +6469,8 @@ async function runBatch(scenarios, options) {
|
|
|
6148
6469
|
finished_at: new Date().toISOString()
|
|
6149
6470
|
});
|
|
6150
6471
|
emit({ type: "run:complete", runId: run.id });
|
|
6472
|
+
const eventType = finalRun.status === "failed" ? "failed" : "completed";
|
|
6473
|
+
dispatchWebhooks(eventType, finalRun).catch(() => {});
|
|
6151
6474
|
return { run: finalRun, results };
|
|
6152
6475
|
}
|
|
6153
6476
|
async function runByFilter(options) {
|
|
@@ -6233,6 +6556,9 @@ function startRunAsync(options) {
|
|
|
6233
6556
|
finished_at: new Date().toISOString()
|
|
6234
6557
|
});
|
|
6235
6558
|
emit({ type: "run:complete", runId: run.id });
|
|
6559
|
+
const asyncRun = getRun(run.id);
|
|
6560
|
+
if (asyncRun)
|
|
6561
|
+
dispatchWebhooks(asyncRun.status === "failed" ? "failed" : "completed", asyncRun).catch(() => {});
|
|
6236
6562
|
} catch (error) {
|
|
6237
6563
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
6238
6564
|
updateRun(run.id, {
|
|
@@ -6240,6 +6566,9 @@ function startRunAsync(options) {
|
|
|
6240
6566
|
finished_at: new Date().toISOString()
|
|
6241
6567
|
});
|
|
6242
6568
|
emit({ type: "run:complete", runId: run.id, error: errorMsg });
|
|
6569
|
+
const failedRun = getRun(run.id);
|
|
6570
|
+
if (failedRun)
|
|
6571
|
+
dispatchWebhooks("failed", failedRun).catch(() => {});
|
|
6243
6572
|
}
|
|
6244
6573
|
})();
|
|
6245
6574
|
return { runId: run.id, scenarioCount: scenarios.length };
|