@hasna/testers 0.0.8 → 0.0.11
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/dashboard/dist/assets/index-DyXKnBM8.css +1 -0
- package/dashboard/dist/assets/index-jNG_Nd_Q.js +49 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cli/index.js +1437 -361
- package/dist/db/results.d.ts +1 -0
- package/dist/db/results.d.ts.map +1 -1
- package/dist/db/runs.d.ts +1 -0
- package/dist/db/runs.d.ts.map +1 -1
- package/dist/db/scenarios.d.ts +5 -0
- package/dist/db/scenarios.d.ts.map +1 -1
- package/dist/db/screenshots.d.ts +1 -0
- package/dist/db/screenshots.d.ts.map +1 -1
- package/dist/index.js +301 -25
- package/dist/lib/costs.d.ts +13 -0
- package/dist/lib/costs.d.ts.map +1 -1
- package/dist/lib/init.d.ts.map +1 -1
- package/dist/lib/logs-integration.d.ts +7 -0
- package/dist/lib/logs-integration.d.ts.map +1 -0
- package/dist/lib/reporter.d.ts +9 -1
- package/dist/lib/reporter.d.ts.map +1 -1
- package/dist/lib/runner.d.ts +7 -1
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/mcp/index.js +78 -5
- package/dist/server/index.js +4257 -19
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/dashboard/dist/assets/index-CDcHt94n.css +0 -1
- package/dashboard/dist/assets/index-DCNDCh61.js +0 -49
package/dist/lib/runner.d.ts
CHANGED
|
@@ -5,13 +5,14 @@ export interface RunOptions {
|
|
|
5
5
|
headed?: boolean;
|
|
6
6
|
parallel?: number;
|
|
7
7
|
timeout?: number;
|
|
8
|
+
retry?: number;
|
|
8
9
|
projectId?: string;
|
|
9
10
|
apiKey?: string;
|
|
10
11
|
screenshotDir?: string;
|
|
11
12
|
engine?: "playwright" | "lightpanda";
|
|
12
13
|
}
|
|
13
14
|
export interface RunEvent {
|
|
14
|
-
type: "scenario:start" | "scenario:pass" | "scenario:fail" | "scenario:error" | "screenshot:captured" | "run:complete" | "step:tool_call" | "step:tool_result" | "step:thinking";
|
|
15
|
+
type: "scenario:start" | "scenario:pass" | "scenario:fail" | "scenario:error" | "screenshot:captured" | "run:complete" | "step:tool_call" | "step:tool_result" | "step:thinking" | "scenario:timeout_warning";
|
|
15
16
|
scenarioId?: string;
|
|
16
17
|
scenarioName?: string;
|
|
17
18
|
resultId?: string;
|
|
@@ -23,6 +24,11 @@ export interface RunEvent {
|
|
|
23
24
|
toolResult?: string;
|
|
24
25
|
thinking?: string;
|
|
25
26
|
stepNumber?: number;
|
|
27
|
+
retryAttempt?: number;
|
|
28
|
+
maxRetries?: number;
|
|
29
|
+
stepDurationMs?: number;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
elapsedMs?: number;
|
|
26
32
|
}
|
|
27
33
|
export type RunEventHandler = (event: RunEvent) => void;
|
|
28
34
|
export declare function onRunEvent(handler: RunEventHandler): void;
|
package/dist/lib/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAa/D,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,YAAY,GAAG,YAAY,CAAC;CACtC;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EACA,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,gBAAgB,GAChB,qBAAqB,GACrB,cAAc,GACd,gBAAgB,GAChB,kBAAkB,GAClB,eAAe,GACf,0BAA0B,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;AAIxD,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAEzD;AA+BD,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,MAAM,CAAC,CAiHjB;AAED,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,QAAQ,EAAE,EACrB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAsI1C;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,UAAU,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GACnF,OAAO,CAAC;IAAE,GAAG,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAuB1C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,UAAU,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GACnF;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAmF1C"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -4813,7 +4813,10 @@ function listScenarios(filter) {
|
|
|
4813
4813
|
if (conditions.length > 0) {
|
|
4814
4814
|
sql += " WHERE " + conditions.join(" AND ");
|
|
4815
4815
|
}
|
|
4816
|
-
|
|
4816
|
+
const sortField = filter?.sort ?? "date";
|
|
4817
|
+
const sortDir = filter?.desc === false ? "ASC" : "DESC";
|
|
4818
|
+
const orderByCol = sortField === "name" ? "name" : sortField === "priority" ? "CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END" : "created_at";
|
|
4819
|
+
sql += ` ORDER BY ${orderByCol} ${sortDir}`;
|
|
4817
4820
|
if (filter?.limit) {
|
|
4818
4821
|
sql += " LIMIT ?";
|
|
4819
4822
|
params.push(filter.limit);
|
|
@@ -4950,7 +4953,10 @@ function listRuns(filter) {
|
|
|
4950
4953
|
if (conditions.length > 0) {
|
|
4951
4954
|
sql += " WHERE " + conditions.join(" AND ");
|
|
4952
4955
|
}
|
|
4953
|
-
|
|
4956
|
+
const sortField = filter?.sort ?? "date";
|
|
4957
|
+
const sortDir = filter?.desc === false ? "ASC" : "DESC";
|
|
4958
|
+
const orderByCol = sortField === "duration" ? "(CASE WHEN finished_at IS NULL THEN NULL ELSE (julianday(finished_at) - julianday(started_at)) * 86400000 END)" : sortField === "cost" ? "(SELECT COALESCE(SUM(cost_cents), 0) FROM results WHERE run_id = runs.id)" : "started_at";
|
|
4959
|
+
sql += ` ORDER BY ${orderByCol} ${sortDir}`;
|
|
4954
4960
|
if (filter?.limit) {
|
|
4955
4961
|
sql += " LIMIT ?";
|
|
4956
4962
|
params.push(filter.limit);
|
|
@@ -6272,6 +6278,38 @@ async function dispatchWebhooks(event, run, schedule) {
|
|
|
6272
6278
|
}
|
|
6273
6279
|
}
|
|
6274
6280
|
|
|
6281
|
+
// src/lib/logs-integration.ts
|
|
6282
|
+
async function pushFailedRunToLogs(run, failedResults, scenarios) {
|
|
6283
|
+
const logsUrl = process.env.LOGS_URL;
|
|
6284
|
+
if (!logsUrl)
|
|
6285
|
+
return;
|
|
6286
|
+
const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
|
|
6287
|
+
const entries = failedResults.map((result) => {
|
|
6288
|
+
const scenario = scenarioMap.get(result.scenarioId);
|
|
6289
|
+
return {
|
|
6290
|
+
level: "error",
|
|
6291
|
+
source: "sdk",
|
|
6292
|
+
service: "testers",
|
|
6293
|
+
message: `[testers] Scenario failed: ${scenario?.name ?? result.scenarioId}${result.error ? ` \u2014 ${result.error}` : ""}`,
|
|
6294
|
+
metadata: {
|
|
6295
|
+
run_id: run.id,
|
|
6296
|
+
scenario_id: result.scenarioId,
|
|
6297
|
+
scenario_name: scenario?.name,
|
|
6298
|
+
url: run.url,
|
|
6299
|
+
status: result.status,
|
|
6300
|
+
duration_ms: result.durationMs
|
|
6301
|
+
}
|
|
6302
|
+
};
|
|
6303
|
+
});
|
|
6304
|
+
try {
|
|
6305
|
+
await fetch(`${logsUrl.replace(/\/$/, "")}/api/logs`, {
|
|
6306
|
+
method: "POST",
|
|
6307
|
+
headers: { "Content-Type": "application/json" },
|
|
6308
|
+
body: JSON.stringify(entries)
|
|
6309
|
+
});
|
|
6310
|
+
} catch {}
|
|
6311
|
+
}
|
|
6312
|
+
|
|
6275
6313
|
// src/lib/runner.ts
|
|
6276
6314
|
var eventHandler = null;
|
|
6277
6315
|
function emit(event) {
|
|
@@ -6280,14 +6318,26 @@ function emit(event) {
|
|
|
6280
6318
|
}
|
|
6281
6319
|
function withTimeout(promise, ms, label) {
|
|
6282
6320
|
return new Promise((resolve, reject) => {
|
|
6321
|
+
const warningAt = Math.floor(ms * 0.8);
|
|
6322
|
+
const warningTimer = setTimeout(() => {
|
|
6323
|
+
emit({
|
|
6324
|
+
type: "scenario:timeout_warning",
|
|
6325
|
+
scenarioName: label,
|
|
6326
|
+
timeoutMs: ms,
|
|
6327
|
+
elapsedMs: warningAt
|
|
6328
|
+
});
|
|
6329
|
+
}, warningAt);
|
|
6283
6330
|
const timer = setTimeout(() => {
|
|
6284
|
-
|
|
6331
|
+
clearTimeout(warningTimer);
|
|
6332
|
+
reject(new Error(`Scenario '${label}' timed out after ${ms}ms. Try: testers run --timeout ${ms * 2} or simplify the scenario steps.`));
|
|
6285
6333
|
}, ms);
|
|
6286
6334
|
promise.then((val) => {
|
|
6287
6335
|
clearTimeout(timer);
|
|
6336
|
+
clearTimeout(warningTimer);
|
|
6288
6337
|
resolve(val);
|
|
6289
6338
|
}, (err) => {
|
|
6290
6339
|
clearTimeout(timer);
|
|
6340
|
+
clearTimeout(warningTimer);
|
|
6291
6341
|
reject(err);
|
|
6292
6342
|
});
|
|
6293
6343
|
});
|
|
@@ -6316,6 +6366,7 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
6316
6366
|
const targetUrl = scenario.targetPath ? `${options.url.replace(/\/$/, "")}${scenario.targetPath}` : options.url;
|
|
6317
6367
|
const scenarioTimeout = scenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
|
|
6318
6368
|
await page.goto(targetUrl, { timeout: Math.min(scenarioTimeout, 30000) });
|
|
6369
|
+
const stepStartTimes = new Map;
|
|
6319
6370
|
const agentResult = await withTimeout(runAgentLoop({
|
|
6320
6371
|
client,
|
|
6321
6372
|
page,
|
|
@@ -6325,6 +6376,16 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
6325
6376
|
runId,
|
|
6326
6377
|
maxTurns: 30,
|
|
6327
6378
|
onStep: (stepEvent) => {
|
|
6379
|
+
let stepDurationMs;
|
|
6380
|
+
if (stepEvent.type === "tool_call") {
|
|
6381
|
+
stepStartTimes.set(stepEvent.stepNumber, Date.now());
|
|
6382
|
+
} else if (stepEvent.type === "tool_result") {
|
|
6383
|
+
const startTime = stepStartTimes.get(stepEvent.stepNumber);
|
|
6384
|
+
if (startTime !== undefined) {
|
|
6385
|
+
stepDurationMs = Date.now() - startTime;
|
|
6386
|
+
stepStartTimes.delete(stepEvent.stepNumber);
|
|
6387
|
+
}
|
|
6388
|
+
}
|
|
6328
6389
|
emit({
|
|
6329
6390
|
type: `step:${stepEvent.type}`,
|
|
6330
6391
|
scenarioId: scenario.id,
|
|
@@ -6334,7 +6395,8 @@ async function runSingleScenario(scenario, runId, options) {
|
|
|
6334
6395
|
toolInput: stepEvent.toolInput,
|
|
6335
6396
|
toolResult: stepEvent.toolResult,
|
|
6336
6397
|
thinking: stepEvent.thinking,
|
|
6337
|
-
stepNumber: stepEvent.stepNumber
|
|
6398
|
+
stepNumber: stepEvent.stepNumber,
|
|
6399
|
+
stepDurationMs
|
|
6338
6400
|
});
|
|
6339
6401
|
}
|
|
6340
6402
|
}), scenarioTimeout, scenario.name);
|
|
@@ -6414,6 +6476,7 @@ async function runBatch(scenarios, options) {
|
|
|
6414
6476
|
} catch {}
|
|
6415
6477
|
return true;
|
|
6416
6478
|
};
|
|
6479
|
+
const maxRetries = options.retry ?? 0;
|
|
6417
6480
|
if (parallel <= 1) {
|
|
6418
6481
|
for (const scenario of sortedScenarios) {
|
|
6419
6482
|
if (!await canRun(scenario)) {
|
|
@@ -6424,7 +6487,13 @@ async function runBatch(scenarios, options) {
|
|
|
6424
6487
|
emit({ type: "scenario:error", scenarioId: scenario.id, scenarioName: scenario.name, error: "Dependency failed \u2014 skipped", runId: run.id });
|
|
6425
6488
|
continue;
|
|
6426
6489
|
}
|
|
6427
|
-
|
|
6490
|
+
let result = await runSingleScenario(scenario, run.id, options);
|
|
6491
|
+
let attempt = 1;
|
|
6492
|
+
while ((result.status === "failed" || result.status === "error") && attempt <= maxRetries) {
|
|
6493
|
+
emit({ type: "scenario:start", scenarioId: scenario.id, scenarioName: scenario.name, runId: run.id, retryAttempt: attempt + 1, maxRetries: maxRetries + 1 });
|
|
6494
|
+
result = await runSingleScenario(scenario, run.id, options);
|
|
6495
|
+
attempt++;
|
|
6496
|
+
}
|
|
6428
6497
|
results.push(result);
|
|
6429
6498
|
if (result.status === "failed" || result.status === "error") {
|
|
6430
6499
|
failedScenarioIds.add(scenario.id);
|
|
@@ -6471,6 +6540,10 @@ async function runBatch(scenarios, options) {
|
|
|
6471
6540
|
emit({ type: "run:complete", runId: run.id });
|
|
6472
6541
|
const eventType = finalRun.status === "failed" ? "failed" : "completed";
|
|
6473
6542
|
dispatchWebhooks(eventType, finalRun).catch(() => {});
|
|
6543
|
+
if (finalRun.status === "failed") {
|
|
6544
|
+
const failedResults = results.filter((r) => r.status === "failed" || r.status === "error");
|
|
6545
|
+
pushFailedRunToLogs(finalRun, failedResults, scenarios).catch(() => {});
|
|
6546
|
+
}
|
|
6474
6547
|
return { run: finalRun, results };
|
|
6475
6548
|
}
|
|
6476
6549
|
async function runByFilter(options) {
|