@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.
@@ -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;
@@ -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;AAY/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,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,CAAC;IACpB,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;CACrB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;AAIxD,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAEzD;AAkBD,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,MAAM,CAAC,CAmGjB;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,CAwH1C;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"}
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
- sql += " ORDER BY created_at DESC";
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
- sql += " ORDER BY started_at DESC";
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
- reject(new Error(`Scenario timeout after ${ms}ms: ${label}`));
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
- const result = await runSingleScenario(scenario, run.id, options);
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) {