@hasna/testers 0.0.8 → 0.0.10

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.
@@ -17,4 +17,5 @@ export declare function updateResult(id: string, updates: Partial<{
17
17
  costCents: number;
18
18
  }>): Result;
19
19
  export declare function getResultsByRun(runId: string): Result[];
20
+ export declare function countResultsByRun(runId: string): number;
20
21
  //# sourceMappingURL=results.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../src/db/results.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,YAAY,EAElB,MAAM,mBAAmB,CAAC;AAG3B,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,MAAM,CAkBT;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAanD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAMnD;AAED,wBAAgB,YAAY,CAC1B,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,GACD,MAAM,CA+CR;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvD"}
1
+ {"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../src/db/results.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,YAAY,EAElB,MAAM,mBAAmB,CAAC;AAG3B,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,MAAM,CAkBT;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAanD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAMnD;AAED,wBAAgB,YAAY,CAC1B,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,GACD,MAAM,CA+CR;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMvD"}
package/dist/db/runs.d.ts CHANGED
@@ -4,6 +4,7 @@ export declare function createRun(input: CreateRunInput & {
4
4
  }): Run;
5
5
  export declare function getRun(id: string): Run | null;
6
6
  export declare function listRuns(filter?: RunFilter): Run[];
7
+ export declare function countRuns(filter?: RunFilter): number;
7
8
  export declare function updateRun(id: string, updates: Partial<RunRow>): Run;
8
9
  export declare function deleteRun(id: string): boolean;
9
10
  //# sourceMappingURL=runs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runs.d.ts","sourceRoot":"","sources":["../../src/db/runs.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,GAAG,EACR,KAAK,MAAM,EACX,KAAK,cAAc,EACnB,KAAK,SAAS,EAEf,MAAM,mBAAmB,CAAC;AAG3B,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,GAAG,CAoBxE;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,CAc7C;AAED,wBAAgB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,GAAG,EAAE,CAgClD;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,CAmEnE;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAO7C"}
1
+ {"version":3,"file":"runs.d.ts","sourceRoot":"","sources":["../../src/db/runs.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,GAAG,EACR,KAAK,MAAM,EACX,KAAK,cAAc,EACnB,KAAK,SAAS,EAEf,MAAM,mBAAmB,CAAC;AAG3B,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,GAAG,CAoBxE;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,CAc7C;AAED,wBAAgB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,GAAG,EAAE,CAgClD;AAED,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,MAAM,CAmBpD;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,GAAG,CAmEnE;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAO7C"}
@@ -4,5 +4,6 @@ export declare function getScenario(id: string): Scenario | null;
4
4
  export declare function getScenarioByShortId(shortId: string): Scenario | null;
5
5
  export declare function listScenarios(filter?: ScenarioFilter): Scenario[];
6
6
  export declare function updateScenario(id: string, input: UpdateScenarioInput, version: number): Scenario;
7
+ export declare function countScenarios(filter?: ScenarioFilter): number;
7
8
  export declare function deleteScenario(id: string): boolean;
8
9
  //# sourceMappingURL=scenarios.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scenarios.d.ts","sourceRoot":"","sources":["../../src/db/scenarios.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EAEb,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAGpB,MAAM,mBAAmB,CAAC;AAsB3B,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,QAAQ,CA8BnE;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAmBvD;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAIrE;AAED,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,QAAQ,EAAE,CA6CjE;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ,CAoFhG;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAOlD"}
1
+ {"version":3,"file":"scenarios.d.ts","sourceRoot":"","sources":["../../src/db/scenarios.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,QAAQ,EAEb,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EAGpB,MAAM,mBAAmB,CAAC;AAsB3B,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,QAAQ,CA8BnE;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAmBvD;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAIrE;AAED,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,QAAQ,EAAE,CA6CjE;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,GAAG,QAAQ,CAoFhG;AAED,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,MAAM,CA8B9D;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAOlD"}
@@ -13,4 +13,5 @@ export declare function createScreenshot(input: {
13
13
  export declare function getScreenshot(id: string): Screenshot | null;
14
14
  export declare function listScreenshots(resultId: string): Screenshot[];
15
15
  export declare function getScreenshotsByResult(resultId: string): Screenshot[];
16
+ export declare function countScreenshots(resultId: string): number;
16
17
  //# sourceMappingURL=screenshots.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"screenshots.d.ts","sourceRoot":"","sources":["../../src/db/screenshots.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAGhB,MAAM,mBAAmB,CAAC;AAG3B,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,GAAG,UAAU,CAuBb;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAI3D;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAM9D;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAErE"}
1
+ {"version":3,"file":"screenshots.d.ts","sourceRoot":"","sources":["../../src/db/screenshots.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAGhB,MAAM,mBAAmB,CAAC;AAG3B,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,GAAG,UAAU,CAuBb;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAI3D;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAM9D;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAErE;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMzD"}
package/dist/index.js CHANGED
@@ -513,7 +513,8 @@ __export(exports_runs, {
513
513
  listRuns: () => listRuns,
514
514
  getRun: () => getRun,
515
515
  deleteRun: () => deleteRun,
516
- createRun: () => createRun
516
+ createRun: () => createRun,
517
+ countRuns: () => countRuns
517
518
  });
518
519
  function createRun(input) {
519
520
  const db2 = getDatabase();
@@ -566,6 +567,24 @@ function listRuns(filter) {
566
567
  const rows = db2.query(sql).all(...params);
567
568
  return rows.map(runFromRow);
568
569
  }
570
+ function countRuns(filter) {
571
+ const db2 = getDatabase();
572
+ const conditions = [];
573
+ const params = [];
574
+ if (filter?.projectId) {
575
+ conditions.push("project_id = ?");
576
+ params.push(filter.projectId);
577
+ }
578
+ if (filter?.status) {
579
+ conditions.push("status = ?");
580
+ params.push(filter.status);
581
+ }
582
+ let sql = "SELECT COUNT(*) as count FROM runs";
583
+ if (conditions.length > 0)
584
+ sql += " WHERE " + conditions.join(" AND ");
585
+ const row = db2.query(sql).get(...params);
586
+ return row.count;
587
+ }
569
588
  function updateRun(id, updates) {
570
589
  const db2 = getDatabase();
571
590
  const existing = getRun(id);
@@ -2665,6 +2684,38 @@ async function testWebhook(id) {
2665
2684
  }
2666
2685
  }
2667
2686
 
2687
+ // src/lib/logs-integration.ts
2688
+ async function pushFailedRunToLogs(run, failedResults, scenarios) {
2689
+ const logsUrl = process.env.LOGS_URL;
2690
+ if (!logsUrl)
2691
+ return;
2692
+ const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
2693
+ const entries = failedResults.map((result) => {
2694
+ const scenario = scenarioMap.get(result.scenarioId);
2695
+ return {
2696
+ level: "error",
2697
+ source: "sdk",
2698
+ service: "testers",
2699
+ message: `[testers] Scenario failed: ${scenario?.name ?? result.scenarioId}${result.error ? ` \u2014 ${result.error}` : ""}`,
2700
+ metadata: {
2701
+ run_id: run.id,
2702
+ scenario_id: result.scenarioId,
2703
+ scenario_name: scenario?.name,
2704
+ url: run.url,
2705
+ status: result.status,
2706
+ duration_ms: result.durationMs
2707
+ }
2708
+ };
2709
+ });
2710
+ try {
2711
+ await fetch(`${logsUrl.replace(/\/$/, "")}/api/logs`, {
2712
+ method: "POST",
2713
+ headers: { "Content-Type": "application/json" },
2714
+ body: JSON.stringify(entries)
2715
+ });
2716
+ } catch {}
2717
+ }
2718
+
2668
2719
  // src/lib/runner.ts
2669
2720
  var eventHandler = null;
2670
2721
  function onRunEvent(handler) {
@@ -2677,7 +2728,7 @@ function emit(event) {
2677
2728
  function withTimeout(promise, ms, label) {
2678
2729
  return new Promise((resolve, reject) => {
2679
2730
  const timer = setTimeout(() => {
2680
- reject(new Error(`Scenario timeout after ${ms}ms: ${label}`));
2731
+ reject(new Error(`Scenario '${label}' timed out after ${ms}ms. Try: testers run --timeout ${ms * 2} or simplify the scenario steps.`));
2681
2732
  }, ms);
2682
2733
  promise.then((val) => {
2683
2734
  clearTimeout(timer);
@@ -2810,6 +2861,7 @@ async function runBatch(scenarios, options) {
2810
2861
  } catch {}
2811
2862
  return true;
2812
2863
  };
2864
+ const maxRetries = options.retry ?? 0;
2813
2865
  if (parallel <= 1) {
2814
2866
  for (const scenario of sortedScenarios) {
2815
2867
  if (!await canRun(scenario)) {
@@ -2820,7 +2872,13 @@ async function runBatch(scenarios, options) {
2820
2872
  emit({ type: "scenario:error", scenarioId: scenario.id, scenarioName: scenario.name, error: "Dependency failed \u2014 skipped", runId: run.id });
2821
2873
  continue;
2822
2874
  }
2823
- const result = await runSingleScenario(scenario, run.id, options);
2875
+ let result = await runSingleScenario(scenario, run.id, options);
2876
+ let attempt = 1;
2877
+ while ((result.status === "failed" || result.status === "error") && attempt <= maxRetries) {
2878
+ emit({ type: "scenario:start", scenarioId: scenario.id, scenarioName: scenario.name, runId: run.id, retryAttempt: attempt + 1, maxRetries: maxRetries + 1 });
2879
+ result = await runSingleScenario(scenario, run.id, options);
2880
+ attempt++;
2881
+ }
2824
2882
  results.push(result);
2825
2883
  if (result.status === "failed" || result.status === "error") {
2826
2884
  failedScenarioIds.add(scenario.id);
@@ -2867,6 +2925,10 @@ async function runBatch(scenarios, options) {
2867
2925
  emit({ type: "run:complete", runId: run.id });
2868
2926
  const eventType = finalRun.status === "failed" ? "failed" : "completed";
2869
2927
  dispatchWebhooks(eventType, finalRun).catch(() => {});
2928
+ if (finalRun.status === "failed") {
2929
+ const failedResults = results.filter((r) => r.status === "failed" || r.status === "error");
2930
+ pushFailedRunToLogs(finalRun, failedResults, scenarios).catch(() => {});
2931
+ }
2870
2932
  return { run: finalRun, results };
2871
2933
  }
2872
2934
  async function runByFilter(options) {
@@ -3468,6 +3530,10 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
3468
3530
  var source_default = chalk;
3469
3531
 
3470
3532
  // src/lib/reporter.ts
3533
+ init_database();
3534
+ function useEmoji() {
3535
+ return !process.env["NO_COLOR"] && process.argv.indexOf("--no-color") === -1;
3536
+ }
3471
3537
  function formatTerminal(run, results) {
3472
3538
  const lines = [];
3473
3539
  lines.push("");
@@ -3482,21 +3548,22 @@ function formatTerminal(run, results) {
3482
3548
  const screenshotCount = screenshots.length;
3483
3549
  let statusIcon;
3484
3550
  let statusColor;
3551
+ const emoji = useEmoji();
3485
3552
  switch (result.status) {
3486
3553
  case "passed":
3487
- statusIcon = source_default.green("PASS");
3554
+ statusIcon = emoji ? "\u2705" : source_default.green("PASS");
3488
3555
  statusColor = source_default.green;
3489
3556
  break;
3490
3557
  case "failed":
3491
- statusIcon = source_default.red("FAIL");
3558
+ statusIcon = emoji ? "\u274C" : source_default.red("FAIL");
3492
3559
  statusColor = source_default.red;
3493
3560
  break;
3494
3561
  case "error":
3495
- statusIcon = source_default.yellow("ERR ");
3562
+ statusIcon = emoji ? "\u26A0\uFE0F " : source_default.yellow("ERR ");
3496
3563
  statusColor = source_default.yellow;
3497
3564
  break;
3498
3565
  default:
3499
- statusIcon = source_default.dim("SKIP");
3566
+ statusIcon = emoji ? "\u23ED\uFE0F " : source_default.dim("SKIP");
3500
3567
  statusColor = source_default.dim;
3501
3568
  break;
3502
3569
  }
@@ -3509,7 +3576,7 @@ function formatTerminal(run, results) {
3509
3576
  }
3510
3577
  }
3511
3578
  lines.push("");
3512
- lines.push(formatSummary(run));
3579
+ lines.push(formatActionableSummary(run, results));
3513
3580
  lines.push("");
3514
3581
  return lines.join(`
3515
3582
  `);
@@ -3521,6 +3588,30 @@ function formatSummary(run) {
3521
3588
  const totalStr = source_default.dim(` (${run.total} total)`);
3522
3589
  return ` ${passedStr}${failedStr}${totalStr} in ${duration}`;
3523
3590
  }
3591
+ function formatActionableSummary(run, results) {
3592
+ const emoji = useEmoji();
3593
+ const passedCount = results.filter((r) => r.status === "passed").length;
3594
+ const failedCount = results.filter((r) => r.status === "failed" || r.status === "error").length;
3595
+ const shortId = run.id.slice(0, 8);
3596
+ const passStr = `${emoji ? "\u2705" : "PASS"} ${passedCount} passed`;
3597
+ const failStr = failedCount > 0 ? ` ${emoji ? "\u274C" : "FAIL"} ${failedCount} failed` : "";
3598
+ const lines = [];
3599
+ lines.push(` ${source_default.bold(passStr)}${failedCount > 0 ? source_default.bold(failStr) : ""}`);
3600
+ if (failedCount > 0) {
3601
+ lines.push(source_default.dim(` retry failed: testers retry ${shortId} | view: testers results ${shortId}`));
3602
+ } else {
3603
+ lines.push(source_default.dim(` view: testers results ${shortId}`));
3604
+ }
3605
+ const totalCostCents = results.reduce((sum, r) => sum + (r.costCents ?? 0), 0);
3606
+ const totalTokens = results.reduce((sum, r) => sum + (r.tokensUsed ?? 0), 0);
3607
+ if (totalTokens > 0) {
3608
+ const costStr = `$${(totalCostCents / 100).toFixed(4)}`;
3609
+ const tokensStr = totalTokens.toLocaleString();
3610
+ lines.push(source_default.dim(` ${emoji ? "\uD83D\uDCB0" : "cost:"} Cost: ${costStr} (${tokensStr} tokens)`));
3611
+ }
3612
+ return lines.join(`
3613
+ `);
3614
+ }
3524
3615
  function formatJSON(run, results) {
3525
3616
  const output = {
3526
3617
  run: {
@@ -3599,6 +3690,15 @@ function formatRunList(runs) {
3599
3690
  return lines.join(`
3600
3691
  `);
3601
3692
  }
3693
+ function getScenarioRunStats(scenarioId) {
3694
+ const db2 = getDatabase();
3695
+ const lastRow = db2.query("SELECT status FROM results WHERE scenario_id = ? ORDER BY created_at DESC LIMIT 1").get(scenarioId);
3696
+ const statsRow = db2.query("SELECT COUNT(*) as total, SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END) as passed FROM results WHERE scenario_id = ?").get(scenarioId);
3697
+ return {
3698
+ lastStatus: lastRow ? lastRow.status : null,
3699
+ passRate: statsRow && statsRow.total > 0 ? `${statsRow.passed}/${statsRow.total}` : "\u2014"
3700
+ };
3701
+ }
3602
3702
  function formatScenarioList(scenarios) {
3603
3703
  const lines = [];
3604
3704
  lines.push("");
@@ -3613,7 +3713,21 @@ function formatScenarioList(scenarios) {
3613
3713
  for (const s of scenarios) {
3614
3714
  const priorityColor = s.priority === "critical" ? source_default.red : s.priority === "high" ? source_default.yellow : s.priority === "medium" ? source_default.blue : source_default.dim;
3615
3715
  const tags = s.tags.length > 0 ? source_default.dim(` [${s.tags.join(", ")}]`) : "";
3616
- lines.push(` ${source_default.cyan(s.shortId)} ${s.name} ${priorityColor(s.priority)}${tags}`);
3716
+ let lastStatusIcon = source_default.dim("\u2014");
3717
+ let passRateStr = source_default.dim("\u2014");
3718
+ if (s.id) {
3719
+ const stats = getScenarioRunStats(s.id);
3720
+ if (stats.lastStatus === "passed")
3721
+ lastStatusIcon = source_default.green("\u2713");
3722
+ else if (stats.lastStatus === "failed")
3723
+ lastStatusIcon = source_default.red("\u2717");
3724
+ else if (stats.lastStatus === "error")
3725
+ lastStatusIcon = source_default.yellow("!");
3726
+ else if (stats.lastStatus === "skipped")
3727
+ lastStatusIcon = source_default.dim("~");
3728
+ passRateStr = stats.passRate === "\u2014" ? source_default.dim("\u2014") : source_default.dim(stats.passRate);
3729
+ }
3730
+ lines.push(` ${source_default.cyan(s.shortId)} ${s.name} ${priorityColor(s.priority)}${tags} ${lastStatusIcon} ${passRateStr}`);
3617
3731
  }
3618
3732
  lines.push("");
3619
3733
  return lines.join(`
@@ -4029,26 +4143,146 @@ function detectFramework(dir) {
4029
4143
  return null;
4030
4144
  }
4031
4145
  function getStarterScenarios(framework, projectId) {
4146
+ if (framework.name === "Next.js") {
4147
+ const scenarios2 = [
4148
+ {
4149
+ name: "Homepage loads",
4150
+ description: "Navigate to the homepage and verify it loads correctly. Check that the main heading and content are visible, and there are no console errors.",
4151
+ tags: ["smoke"],
4152
+ priority: "high",
4153
+ projectId
4154
+ },
4155
+ {
4156
+ name: "404 page works",
4157
+ description: "Navigate to a non-existent URL (e.g. /this-page-does-not-exist) and verify the Next.js 404 page renders correctly.",
4158
+ tags: ["smoke"],
4159
+ priority: "medium",
4160
+ projectId
4161
+ },
4162
+ {
4163
+ name: "Navigation links work",
4164
+ description: "Click through the main navigation links and verify each page loads without errors. Check that client-side routing is working correctly.",
4165
+ tags: ["smoke"],
4166
+ priority: "medium",
4167
+ projectId
4168
+ }
4169
+ ];
4170
+ if (framework.features.includes("hasAuth")) {
4171
+ scenarios2.push({
4172
+ name: "Login flow",
4173
+ description: "Navigate to the login page, enter valid credentials, and verify successful authentication and redirect.",
4174
+ tags: ["auth"],
4175
+ priority: "critical",
4176
+ projectId
4177
+ }, {
4178
+ name: "Protected route redirect",
4179
+ description: "Try to access a protected route without authentication and verify you are redirected to the login page.",
4180
+ tags: ["auth"],
4181
+ priority: "high",
4182
+ projectId
4183
+ });
4184
+ }
4185
+ if (framework.features.includes("hasForms")) {
4186
+ scenarios2.push({
4187
+ name: "Form validation",
4188
+ description: "Submit forms with empty/invalid data and verify validation errors appear correctly.",
4189
+ tags: ["forms"],
4190
+ priority: "medium",
4191
+ projectId
4192
+ });
4193
+ }
4194
+ return scenarios2;
4195
+ }
4196
+ if (framework.name === "Vite" || framework.name === "SvelteKit") {
4197
+ const scenarios2 = [
4198
+ {
4199
+ name: "Homepage loads",
4200
+ description: "Navigate to the homepage and verify it loads correctly with no console errors.",
4201
+ tags: ["smoke"],
4202
+ priority: "high",
4203
+ projectId
4204
+ },
4205
+ {
4206
+ name: "Mobile viewport check",
4207
+ description: "Set the viewport to 375x812 (iPhone) and verify the homepage renders correctly without horizontal scrolling or layout issues.",
4208
+ tags: ["responsive"],
4209
+ priority: "medium",
4210
+ projectId
4211
+ },
4212
+ {
4213
+ name: "No console errors",
4214
+ description: "Navigate through the app and verify there are no JavaScript errors or warnings in the browser console.",
4215
+ tags: ["smoke"],
4216
+ priority: "high",
4217
+ projectId
4218
+ }
4219
+ ];
4220
+ if (framework.features.includes("hasAuth")) {
4221
+ scenarios2.push({
4222
+ name: "Login flow",
4223
+ description: "Navigate to the login page, enter valid credentials, and verify successful authentication.",
4224
+ tags: ["auth"],
4225
+ priority: "critical",
4226
+ projectId
4227
+ });
4228
+ }
4229
+ return scenarios2;
4230
+ }
4231
+ if (framework.name === "Nuxt") {
4232
+ const scenarios2 = [
4233
+ {
4234
+ name: "Homepage loads",
4235
+ description: "Navigate to the homepage and verify it loads correctly. Check that the main heading and content are visible.",
4236
+ tags: ["smoke"],
4237
+ priority: "high",
4238
+ projectId
4239
+ },
4240
+ {
4241
+ name: "Navigation works",
4242
+ description: "Click through main navigation links and verify each page loads without errors.",
4243
+ tags: ["smoke"],
4244
+ priority: "medium",
4245
+ projectId
4246
+ },
4247
+ {
4248
+ name: "Mobile viewport check",
4249
+ description: "Set the viewport to 375x812 and verify the homepage renders correctly on mobile.",
4250
+ tags: ["responsive"],
4251
+ priority: "medium",
4252
+ projectId
4253
+ }
4254
+ ];
4255
+ if (framework.features.includes("hasAuth")) {
4256
+ scenarios2.push({
4257
+ name: "Login flow",
4258
+ description: "Navigate to the login page, enter valid credentials, and verify successful authentication.",
4259
+ tags: ["auth"],
4260
+ priority: "critical",
4261
+ projectId
4262
+ });
4263
+ }
4264
+ return scenarios2;
4265
+ }
4032
4266
  const scenarios = [
4033
4267
  {
4034
- name: "Landing page loads",
4035
- description: "Navigate to the landing page and verify it loads correctly with no console errors. Check that the main heading, navigation, and primary CTA are visible.",
4268
+ name: "Homepage loads",
4269
+ description: "Navigate to the homepage and verify it loads correctly with no console errors. Check that the main heading, navigation, and primary CTA are visible.",
4036
4270
  tags: ["smoke"],
4037
4271
  priority: "high",
4038
4272
  projectId
4039
4273
  },
4040
4274
  {
4041
- name: "Navigation works",
4042
- description: "Click through main navigation links and verify each page loads without errors.",
4275
+ name: "Form submit works",
4276
+ description: "Find the main form on the page, fill it in with valid test data, submit it, and verify the success state.",
4043
4277
  tags: ["smoke"],
4044
4278
  priority: "medium",
4045
4279
  projectId
4046
4280
  },
4047
4281
  {
4048
- name: "No console errors",
4049
- description: "Navigate through the main pages and check the browser console for any JavaScript errors or warnings.",
4050
- tags: ["smoke"],
4051
- priority: "high",
4282
+ name: "Mobile viewport check",
4283
+ description: "Set the viewport to 375x812 (iPhone) and verify the homepage renders correctly without horizontal scrolling or layout issues.",
4284
+ tags: ["responsive"],
4285
+ priority: "medium",
4052
4286
  projectId
4053
4287
  }
4054
4288
  ];
@@ -4891,12 +5125,13 @@ function formatCostsTerminal(summary) {
4891
5125
  }
4892
5126
  if (summary.byScenario.length > 0) {
4893
5127
  lines.push("");
4894
- lines.push(source_default.bold(" Top Scenarios by Cost"));
4895
- lines.push(` ${"Scenario".padEnd(40)} ${"Cost".padEnd(12)} ${"Tokens".padEnd(12)} Runs`);
4896
- lines.push(` ${"\u2500".repeat(40)} ${"\u2500".repeat(12)} ${"\u2500".repeat(12)} ${"\u2500".repeat(6)}`);
5128
+ lines.push(source_default.bold(" Scenarios by Cost (most expensive first)"));
5129
+ lines.push(` ${"Scenario".padEnd(40)} ${"Total Cost".padEnd(12)} ${"Avg/Run".padEnd(12)} ${"Runs".padEnd(6)} Tokens`);
5130
+ lines.push(` ${"\u2500".repeat(40)} ${"\u2500".repeat(12)} ${"\u2500".repeat(12)} ${"\u2500".repeat(6)} ${"\u2500".repeat(10)}`);
4897
5131
  for (const s of summary.byScenario) {
4898
5132
  const label = s.name.length > 38 ? s.name.slice(0, 35) + "..." : s.name;
4899
- lines.push(` ${label.padEnd(40)} ${formatDollars(s.costCents).padEnd(12)} ${formatTokens(s.tokens).padEnd(12)} ${s.runs}`);
5133
+ const avgPerRun = s.runs > 0 ? s.costCents / s.runs : 0;
5134
+ lines.push(` ${label.padEnd(40)} ${formatDollars(s.costCents).padEnd(12)} ${formatDollars(avgPerRun).padEnd(12)} ${String(s.runs).padEnd(6)} ${formatTokens(s.tokens)}`);
4900
5135
  }
4901
5136
  }
4902
5137
  lines.push("");
@@ -33,4 +33,5 @@ export declare function checkBudget(estimatedCostCents: number): {
33
33
  };
34
34
  export declare function formatCostsTerminal(summary: CostSummary): string;
35
35
  export declare function formatCostsJSON(summary: CostSummary): string;
36
+ export declare function formatCostsCsv(summary: CostSummary): string;
36
37
  //# sourceMappingURL=costs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"costs.d.ts","sourceRoot":"","sources":["../../src/lib/costs.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7E,UAAU,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzG,aAAa,EAAE,MAAM,CAAC;IACtB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AA0CD,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;CAC3C,GAAG,WAAW,CAyFd;AAED,wBAAgB,WAAW,CAAC,kBAAkB,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CA+B9F;AAcD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAwChE;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAE5D"}
1
+ {"version":3,"file":"costs.d.ts","sourceRoot":"","sources":["../../src/lib/costs.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7E,UAAU,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzG,aAAa,EAAE,MAAM,CAAC;IACtB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AA0CD,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;CAC3C,GAAG,WAAW,CAyFd;AAED,wBAAgB,WAAW,CAAC,kBAAkB,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CA+B9F;AAcD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAyChE;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAE5D;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAS3D"}
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/lib/init.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAI7D,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAmDjE;AAID,wBAAgB,mBAAmB,CACjC,SAAS,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,EAC/C,SAAS,EAAE,MAAM,GAChB,mBAAmB,EAAE,CA6DvB;AAID,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;IAC1C,SAAS,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,EAAE,CAAC;IAC/C,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,CAuC5D"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/lib/init.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAI7D,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAmDjE;AAID,wBAAgB,mBAAmB,CACjC,SAAS,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,EAC/C,SAAS,EAAE,MAAM,GAChB,mBAAmB,EAAE,CAkNvB;AAID,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;IAC1C,SAAS,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,EAAE,CAAC;IAC/C,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,CAuC5D"}
@@ -0,0 +1,7 @@
1
+ import type { Run, Result, Scenario } from "../types/index.js";
2
+ /**
3
+ * Push failed test results to open-logs if LOGS_URL env var is set.
4
+ * No-op when LOGS_URL is not configured.
5
+ */
6
+ export declare function pushFailedRunToLogs(run: Run, failedResults: Result[], scenarios: Scenario[]): Promise<void>;
7
+ //# sourceMappingURL=logs-integration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logs-integration.d.ts","sourceRoot":"","sources":["../../src/lib/logs-integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAE/D;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,GAAG,EACR,aAAa,EAAE,MAAM,EAAE,EACvB,SAAS,EAAE,QAAQ,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CAiCf"}
@@ -5,10 +5,17 @@ export interface ReportOptions {
5
5
  }
6
6
  export declare function formatTerminal(run: Run, results: Result[]): string;
7
7
  export declare function formatSummary(run: Run): string;
8
+ export declare function formatActionableSummary(run: Run, results: Result[]): string;
8
9
  export declare function formatJSON(run: Run, results: Result[]): string;
9
10
  export declare function getExitCode(run: Run): number;
10
11
  export declare function formatRunList(runs: Run[]): string;
12
+ export interface ScenarioRunStats {
13
+ lastStatus: "passed" | "failed" | "error" | "skipped" | null;
14
+ passRate: string;
15
+ }
16
+ export declare function getScenarioRunStats(scenarioId: string): ScenarioRunStats;
11
17
  export declare function formatScenarioList(scenarios: Array<{
18
+ id?: string;
12
19
  shortId: string;
13
20
  name: string;
14
21
  priority: string;
@@ -1 +1 @@
1
- {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../../src/lib/reporter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAIjE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAmDlE;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAU9C;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAoD9D;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAI5C;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CA6BjD;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,GAAG,MAAM,CA2BhI;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,MAAM,CAqCpF"}
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../../src/lib/reporter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAWjE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAoDlE;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAU9C;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CA4B3E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAoD9D;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAI5C;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CA6BjD;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC;IAC7D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,gBAAgB,CAiBxE;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,KAAK,CAAC;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,GAAG,MAAM,CAwC7I;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,MAAM,CAqCpF"}
@@ -5,6 +5,7 @@ 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;
@@ -23,6 +24,8 @@ export interface RunEvent {
23
24
  toolResult?: string;
24
25
  thinking?: string;
25
26
  stepNumber?: number;
27
+ retryAttempt?: number;
28
+ maxRetries?: number;
26
29
  }
27
30
  export type RunEventHandler = (event: RunEvent) => void;
28
31
  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,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;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,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,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
@@ -6272,6 +6272,38 @@ async function dispatchWebhooks(event, run, schedule) {
6272
6272
  }
6273
6273
  }
6274
6274
 
6275
+ // src/lib/logs-integration.ts
6276
+ async function pushFailedRunToLogs(run, failedResults, scenarios) {
6277
+ const logsUrl = process.env.LOGS_URL;
6278
+ if (!logsUrl)
6279
+ return;
6280
+ const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
6281
+ const entries = failedResults.map((result) => {
6282
+ const scenario = scenarioMap.get(result.scenarioId);
6283
+ return {
6284
+ level: "error",
6285
+ source: "sdk",
6286
+ service: "testers",
6287
+ message: `[testers] Scenario failed: ${scenario?.name ?? result.scenarioId}${result.error ? ` \u2014 ${result.error}` : ""}`,
6288
+ metadata: {
6289
+ run_id: run.id,
6290
+ scenario_id: result.scenarioId,
6291
+ scenario_name: scenario?.name,
6292
+ url: run.url,
6293
+ status: result.status,
6294
+ duration_ms: result.durationMs
6295
+ }
6296
+ };
6297
+ });
6298
+ try {
6299
+ await fetch(`${logsUrl.replace(/\/$/, "")}/api/logs`, {
6300
+ method: "POST",
6301
+ headers: { "Content-Type": "application/json" },
6302
+ body: JSON.stringify(entries)
6303
+ });
6304
+ } catch {}
6305
+ }
6306
+
6275
6307
  // src/lib/runner.ts
6276
6308
  var eventHandler = null;
6277
6309
  function emit(event) {
@@ -6281,7 +6313,7 @@ function emit(event) {
6281
6313
  function withTimeout(promise, ms, label) {
6282
6314
  return new Promise((resolve, reject) => {
6283
6315
  const timer = setTimeout(() => {
6284
- reject(new Error(`Scenario timeout after ${ms}ms: ${label}`));
6316
+ reject(new Error(`Scenario '${label}' timed out after ${ms}ms. Try: testers run --timeout ${ms * 2} or simplify the scenario steps.`));
6285
6317
  }, ms);
6286
6318
  promise.then((val) => {
6287
6319
  clearTimeout(timer);
@@ -6414,6 +6446,7 @@ async function runBatch(scenarios, options) {
6414
6446
  } catch {}
6415
6447
  return true;
6416
6448
  };
6449
+ const maxRetries = options.retry ?? 0;
6417
6450
  if (parallel <= 1) {
6418
6451
  for (const scenario of sortedScenarios) {
6419
6452
  if (!await canRun(scenario)) {
@@ -6424,7 +6457,13 @@ async function runBatch(scenarios, options) {
6424
6457
  emit({ type: "scenario:error", scenarioId: scenario.id, scenarioName: scenario.name, error: "Dependency failed \u2014 skipped", runId: run.id });
6425
6458
  continue;
6426
6459
  }
6427
- const result = await runSingleScenario(scenario, run.id, options);
6460
+ let result = await runSingleScenario(scenario, run.id, options);
6461
+ let attempt = 1;
6462
+ while ((result.status === "failed" || result.status === "error") && attempt <= maxRetries) {
6463
+ emit({ type: "scenario:start", scenarioId: scenario.id, scenarioName: scenario.name, runId: run.id, retryAttempt: attempt + 1, maxRetries: maxRetries + 1 });
6464
+ result = await runSingleScenario(scenario, run.id, options);
6465
+ attempt++;
6466
+ }
6428
6467
  results.push(result);
6429
6468
  if (result.status === "failed" || result.status === "error") {
6430
6469
  failedScenarioIds.add(scenario.id);
@@ -6471,6 +6510,10 @@ async function runBatch(scenarios, options) {
6471
6510
  emit({ type: "run:complete", runId: run.id });
6472
6511
  const eventType = finalRun.status === "failed" ? "failed" : "completed";
6473
6512
  dispatchWebhooks(eventType, finalRun).catch(() => {});
6513
+ if (finalRun.status === "failed") {
6514
+ const failedResults = results.filter((r) => r.status === "failed" || r.status === "error");
6515
+ pushFailedRunToLogs(finalRun, failedResults, scenarios).catch(() => {});
6516
+ }
6474
6517
  return { run: finalRun, results };
6475
6518
  }
6476
6519
  async function runByFilter(options) {