@vitest-evals/github-reporter 0.16.1 → 0.17.0

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/index.mjs CHANGED
@@ -93,7 +93,8 @@ function collectEvalReport(input, options = {}) {
93
93
  const cases = workspace.cases.map(collectEvalCase);
94
94
  const failures = cases.filter((testCase) => testCase.status === "failed");
95
95
  const evalScores = cases.map((testCase) => testCase.eval?.avgScore).filter((score) => isFiniteNumber(score));
96
- const usage = sumUsage(cases);
96
+ const usage = sumAppUsage(cases);
97
+ const judgeUsage = sumJudgeUsage(cases);
97
98
  const durationMs = workspace.runs[0]?.durationMs;
98
99
  return {
99
100
  status: input.success && failures.length === 0 ? "passed" : "failed",
@@ -113,6 +114,7 @@ function collectEvalReport(input, options = {}) {
113
114
  minimum: Math.min(...evalScores)
114
115
  } : void 0,
115
116
  usage,
117
+ judgeUsage,
116
118
  cases,
117
119
  failures
118
120
  };
@@ -237,24 +239,67 @@ function stringifyReason(value) {
237
239
  }
238
240
  return typeof value === "string" ? value : stringifyValue(value, 4e3);
239
241
  }
240
- function sumUsage(cases) {
241
- const usage = {
242
+ function emptyUsage() {
243
+ return {
242
244
  inputTokens: 0,
243
245
  outputTokens: 0,
244
246
  reasoningTokens: 0,
245
247
  totalTokens: 0,
246
248
  toolCalls: 0
247
249
  };
250
+ }
251
+ function addRunUsage(total, usage) {
252
+ total.inputTokens += usage?.inputTokens ?? 0;
253
+ total.outputTokens += usage?.outputTokens ?? 0;
254
+ total.reasoningTokens += usage?.reasoningTokens ?? 0;
255
+ total.totalTokens += usage?.totalTokens ?? (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0) + (usage?.reasoningTokens ?? 0);
256
+ if (usage?.costUsd !== void 0) {
257
+ total.costUsd = (total.costUsd ?? 0) + usage.costUsd;
258
+ }
259
+ total.toolCalls += usage?.toolCalls ?? 0;
260
+ }
261
+ function sumAppUsage(cases) {
262
+ const usage = emptyUsage();
263
+ const runUsages = cases.map(appUsageForCase).filter((item) => item !== void 0);
264
+ for (const runUsage of runUsages) {
265
+ addRunUsage(usage, runUsage);
266
+ }
267
+ omitPartialCost(usage, runUsages);
268
+ return usage;
269
+ }
270
+ function appUsageForCase(testCase) {
271
+ const usage = testCase.harness?.usage;
272
+ const effectiveToolCalls = toolCallCount(testCase);
273
+ if (!usage && effectiveToolCalls === 0) {
274
+ return void 0;
275
+ }
276
+ return {
277
+ ...usage,
278
+ ...effectiveToolCalls > 0 ? { toolCalls: effectiveToolCalls } : {}
279
+ };
280
+ }
281
+ function sumJudgeUsage(cases) {
282
+ const usage = emptyUsage();
283
+ const runUsages = [];
248
284
  for (const testCase of cases) {
249
- const caseUsage = testCase.harness?.usage;
250
- usage.inputTokens += caseUsage?.inputTokens ?? 0;
251
- usage.outputTokens += caseUsage?.outputTokens ?? 0;
252
- usage.reasoningTokens += caseUsage?.reasoningTokens ?? 0;
253
- usage.totalTokens += caseUsage?.totalTokens ?? (caseUsage?.inputTokens ?? 0) + (caseUsage?.outputTokens ?? 0) + (caseUsage?.reasoningTokens ?? 0);
254
- usage.toolCalls += toolCallCount(testCase);
285
+ for (const score of testCase.eval?.scores ?? []) {
286
+ for (const run of score.judgeRuns ?? []) {
287
+ runUsages.push(run.usage);
288
+ addRunUsage(usage, run.usage);
289
+ }
290
+ }
255
291
  }
292
+ omitPartialCost(usage, runUsages);
256
293
  return usage;
257
294
  }
295
+ function omitPartialCost(total, usages) {
296
+ if (usages.some(hasUsageWithoutCost)) {
297
+ total.costUsd = void 0;
298
+ }
299
+ }
300
+ function hasUsageWithoutCost(usage) {
301
+ return usage?.costUsd === void 0 && ((usage?.totalTokens ?? (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0) + (usage?.reasoningTokens ?? 0)) > 0 || (usage?.toolCalls ?? 0) > 0);
302
+ }
258
303
  function toolCallCount(testCase) {
259
304
  const usageToolCalls = testCase.harness?.usage?.toolCalls;
260
305
  if (usageToolCalls !== void 0) {
@@ -398,8 +443,8 @@ function evaluateEvalGate(report, policy = {}) {
398
443
  status: "failed",
399
444
  enforced: true,
400
445
  passRate,
401
- title: "Eval report hard failure",
402
- message: `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}; ${counts}`
446
+ title: "Eval run failed",
447
+ message: `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases; ${counts}`
403
448
  };
404
449
  }
405
450
  if (report.totals.evalTotal === 0) {
@@ -408,7 +453,7 @@ function evaluateEvalGate(report, policy = {}) {
408
453
  status: "failed",
409
454
  enforced: true,
410
455
  passRate: null,
411
- title: "Eval report hard failure",
456
+ title: "Eval run failed",
412
457
  message: "no eval cases were reported"
413
458
  };
414
459
  }
@@ -418,8 +463,8 @@ function evaluateEvalGate(report, policy = {}) {
418
463
  status: "failed",
419
464
  enforced: true,
420
465
  passRate,
421
- title: "Eval report hard failure",
422
- message: `vitest run failed without counted test failures; ${counts}`
466
+ title: "Eval run failed",
467
+ message: `Vitest failed without reporting a failed test; ${counts}`
423
468
  };
424
469
  }
425
470
  if (minPassRate !== void 0 && (passRate === null || passRate + Number.EPSILON < minPassRate)) {
@@ -429,7 +474,7 @@ function evaluateEvalGate(report, policy = {}) {
429
474
  enforced: true,
430
475
  passRate,
431
476
  title: `Eval pass rate ${formatPercent(passRate)} \u2014 required ${formatPercent(minPassRate)}`,
432
- message: `eval pass rate below floor: ${counts}; required >= ${formatPercent(minPassRate)}`
477
+ message: `pass rate is below the minimum: ${counts}; minimum ${formatPercent(minPassRate)}`
433
478
  };
434
479
  }
435
480
  if (minScoreAverage !== void 0) {
@@ -440,8 +485,8 @@ function evaluateEvalGate(report, policy = {}) {
440
485
  status: "failed",
441
486
  enforced: true,
442
487
  passRate,
443
- title: "Eval score gate failed",
444
- message: `no score average available; required avg score >= ${formatScore(minScoreAverage)}`
488
+ title: "Average score unavailable",
489
+ message: `no average score was reported; minimum ${formatScore(minScoreAverage)}`
445
490
  };
446
491
  }
447
492
  if (average + Number.EPSILON < minScoreAverage) {
@@ -450,8 +495,8 @@ function evaluateEvalGate(report, policy = {}) {
450
495
  status: "failed",
451
496
  enforced: true,
452
497
  passRate,
453
- title: `Avg score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
454
- message: `avg score below floor: ${counts}; required avg score >= ${formatScore(minScoreAverage)}`
498
+ title: `Average score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
499
+ message: `average score is below the minimum: ${counts}; minimum ${formatScore(minScoreAverage)}`
455
500
  };
456
501
  }
457
502
  }
@@ -461,7 +506,7 @@ function evaluateEvalGate(report, policy = {}) {
461
506
  enforced: true,
462
507
  passRate,
463
508
  title: enforcedPassTitle(report, passRate, minPassRate, minScoreAverage),
464
- message: `eval gate passed: ${counts}${formatFloorSuffix(minPassRate, minScoreAverage)}`
509
+ message: `requirements met: ${counts}${formatMinimumSuffix(minPassRate, minScoreAverage)}`
465
510
  };
466
511
  }
467
512
  function computePassRate(report) {
@@ -494,7 +539,7 @@ function resolveMinPassRate(policy) {
494
539
  }
495
540
  function defaultCheckTitle(report) {
496
541
  if (report.failures.length === 0 && report.status === "passed") {
497
- return "No eval failures";
542
+ return "No eval cases failed";
498
543
  }
499
544
  if (report.failures.length === 0) {
500
545
  return "Vitest run failed";
@@ -503,10 +548,10 @@ function defaultCheckTitle(report) {
503
548
  }
504
549
  function enforcedPassTitle(report, passRate, minPassRate, minScoreAverage) {
505
550
  if (minPassRate !== void 0) {
506
- return `Eval pass rate ${formatPercent(passRate)} \u2014 floor ${formatPercent(minPassRate)}`;
551
+ return `Eval pass rate ${formatPercent(passRate)} \u2014 minimum ${formatPercent(minPassRate)}`;
507
552
  }
508
553
  if (minScoreAverage !== void 0) {
509
- return `Avg score ${formatScore(report.score?.average)} \u2014 floor ${formatScore(minScoreAverage)}`;
554
+ return `Average score ${formatScore(report.score?.average)} \u2014 minimum ${formatScore(minScoreAverage)}`;
510
555
  }
511
556
  return defaultCheckTitle(report);
512
557
  }
@@ -515,15 +560,15 @@ function formatEvalCounts(report, passRate) {
515
560
  const passRateText = passRate === null ? "n/a" : formatPercent(passRate);
516
561
  return `${formatNumber(report.totals.evalPassed)}/${formatNumber(
517
562
  report.totals.evalTotal
518
- )} passed (${passRateText}), avg score ${scoreText}`;
563
+ )} passed (${passRateText}), average score ${scoreText}`;
519
564
  }
520
- function formatFloorSuffix(minPassRate, minScoreAverage) {
565
+ function formatMinimumSuffix(minPassRate, minScoreAverage) {
521
566
  const parts = [];
522
567
  if (minPassRate !== void 0) {
523
- parts.push(`pass rate floor ${formatPercent(minPassRate)}`);
568
+ parts.push(`minimum pass rate ${formatPercent(minPassRate)}`);
524
569
  }
525
570
  if (minScoreAverage !== void 0) {
526
- parts.push(`avg score floor ${formatScore(minScoreAverage)}`);
571
+ parts.push(`minimum average score ${formatScore(minScoreAverage)}`);
527
572
  }
528
573
  return parts.length === 0 ? "" : `; ${parts.join(", ")}`;
529
574
  }
@@ -558,23 +603,23 @@ function renderJobSummary(report, options = {}) {
558
603
  ""
559
604
  ];
560
605
  if (report.failures.length > 0) {
561
- const failureHeading = options.gate?.ok === true ? "### Quality Misses" : "### Failures";
606
+ const failureHeading = options.gate?.ok === true ? "### Cases Below Target" : "### Failures";
562
607
  lines.push(failureHeading, "");
563
608
  failures.forEach((testCase, index) => {
564
609
  lines.push(...renderFailureDetails(testCase, index + 1, options), "");
565
610
  });
566
611
  if (report.failures.length > failures.length) {
567
- const omittedLabel = options.gate?.ok === true ? "quality misses" : "failures";
612
+ const omittedLabel = options.gate?.ok === true ? "cases below target" : "failures";
568
613
  lines.push(
569
614
  `${report.failures.length - failures.length} more ${omittedLabel} omitted from this summary.`,
570
615
  ""
571
616
  );
572
617
  }
573
618
  } else if (report.totals.evalTotal > 0) {
574
- lines.push("### Failures", "", "No eval failures.", "");
619
+ lines.push("### Failures", "", "No eval cases failed.", "");
575
620
  }
576
621
  if (report.totals.evalTotal === 0) {
577
- lines.push("No eval metadata was found in the Vitest JSON report.", "");
622
+ lines.push("No eval results were found in the Vitest JSON report.", "");
578
623
  }
579
624
  return `${lines.join("\n")}
580
625
  `;
@@ -584,9 +629,9 @@ function formatCountLine(passed, failed, total) {
584
629
  }
585
630
  function renderSummaryTable(report, nonEvalFailures, gate) {
586
631
  const rows = [
587
- ["Status", gate?.status ?? report.status],
632
+ ["Status", capitalize(gate?.status ?? report.status)],
588
633
  [
589
- "Evals",
634
+ "Eval cases",
590
635
  formatCountLine(
591
636
  report.totals.evalPassed,
592
637
  report.totals.evalFailed,
@@ -595,23 +640,31 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
595
640
  ]
596
641
  ];
597
642
  if (gate?.passRate !== void 0 && gate.passRate !== null) {
598
- rows.push(["Pass Rate", formatPercent(gate.passRate)]);
643
+ rows.push(["Pass rate", formatPercent(gate.passRate)]);
599
644
  } else if (report.totals.evalTotal > 0) {
600
645
  rows.push([
601
- "Pass Rate",
646
+ "Pass rate",
602
647
  formatPercent(report.totals.evalPassed / report.totals.evalTotal)
603
648
  ]);
604
649
  }
605
650
  if (report.score) {
606
651
  rows.push(["Score", formatScoreSummary(report.score)]);
607
652
  }
653
+ if (hasUsage(report.usage) || hasUsage(report.judgeUsage)) {
654
+ rows.push(["App usage", formatUsage(report.usage)]);
655
+ rows.push(["Judge usage", formatUsage(report.judgeUsage)]);
656
+ rows.push([
657
+ "Total usage",
658
+ formatUsage(sumUsage(report.usage, report.judgeUsage))
659
+ ]);
660
+ }
608
661
  if (gate?.enforced) {
609
- rows.push(["Gate", gate.message]);
662
+ rows.push(["Requirements", gate.message]);
610
663
  }
611
664
  if (nonEvalFailures > 0) {
612
665
  rows.push([
613
- "Other Failures",
614
- `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}`
666
+ "Other test failures",
667
+ `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases`
615
668
  ]);
616
669
  }
617
670
  rows.push(["Duration", formatDuration(report.durationMs)]);
@@ -624,7 +677,53 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
624
677
  ];
625
678
  }
626
679
  function formatScoreSummary(score) {
627
- return `avg ${formatScore(score.average)}${score.minimum === void 0 ? "" : `, min ${formatScore(score.minimum)}`}`;
680
+ return `average ${formatScore(score.average)}${score.minimum === void 0 ? "" : `, lowest ${formatScore(score.minimum)}`}`;
681
+ }
682
+ function capitalize(value) {
683
+ return value.charAt(0).toUpperCase() + value.slice(1);
684
+ }
685
+ function hasUsage(usage) {
686
+ return usage.totalTokens > 0 || usage.costUsd !== void 0 || usage.toolCalls > 0;
687
+ }
688
+ function sumUsage(app, judge) {
689
+ const appCostKnown = !hasUsageWithoutCost2(app);
690
+ const judgeCostKnown = !hasUsageWithoutCost2(judge);
691
+ return {
692
+ inputTokens: app.inputTokens + judge.inputTokens,
693
+ outputTokens: app.outputTokens + judge.outputTokens,
694
+ reasoningTokens: app.reasoningTokens + judge.reasoningTokens,
695
+ totalTokens: app.totalTokens + judge.totalTokens,
696
+ ...appCostKnown && judgeCostKnown ? { costUsd: (app.costUsd ?? 0) + (judge.costUsd ?? 0) } : {},
697
+ toolCalls: app.toolCalls + judge.toolCalls
698
+ };
699
+ }
700
+ function hasUsageWithoutCost2(usage) {
701
+ return usage.costUsd === void 0 && (usage.totalTokens > 0 || usage.toolCalls > 0);
702
+ }
703
+ function formatUsage(usage) {
704
+ const parts = [];
705
+ if (usage.totalTokens > 0) {
706
+ parts.push(`${formatNumber(usage.totalTokens)} tokens`);
707
+ }
708
+ if (usage.costUsd !== void 0) {
709
+ parts.push(
710
+ usage.costUsd.toLocaleString("en-US", {
711
+ style: "currency",
712
+ currency: "USD",
713
+ minimumFractionDigits: 2,
714
+ maximumFractionDigits: 6
715
+ })
716
+ );
717
+ }
718
+ if (hasUsageWithoutCost2(usage)) {
719
+ parts.push("cost unavailable");
720
+ }
721
+ if (usage.toolCalls > 0) {
722
+ parts.push(
723
+ `${formatNumber(usage.toolCalls)} tool call${usage.toolCalls === 1 ? "" : "s"}`
724
+ );
725
+ }
726
+ return parts.join(", ") || "none";
628
727
  }
629
728
  function escapeTableCell(value) {
630
729
  return value.replace(/\r?\n/g, " ").replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
@@ -704,7 +803,7 @@ function renderFailureBlock(testCase, {
704
803
  ["Case", `${number}. ${testCase.displayName}`],
705
804
  ["Status", testCase.status],
706
805
  ["Location", formatLocation(testCase.displayFile, testCase.location)],
707
- ["Harness", testCase.harness?.name ?? "n/a"],
806
+ ["App runner", testCase.harness?.name ?? "n/a"],
708
807
  ["Score", formatScore(failure?.score ?? testCase.eval?.avgScore)],
709
808
  ["Judge", failure?.judgeName ?? "n/a"]
710
809
  ];
@@ -742,7 +841,7 @@ function renderFailureBlock(testCase, {
742
841
  if (finalOutput !== void 0) {
743
842
  lines.push(
744
843
  ...renderAsciiSection(
745
- "Final Output",
844
+ "Output",
746
845
  stringifyValue(finalOutput, maxOutputChars).split(/\r?\n/)
747
846
  ),
748
847
  ""
@@ -770,7 +869,7 @@ function renderFailureBlock(testCase, {
770
869
  if (testCase.harness?.errors.length) {
771
870
  lines.push(
772
871
  ...renderAsciiSection(
773
- "Harness Errors",
872
+ "App errors",
774
873
  stringifyValue(testCase.harness.errors, maxReasonChars).split(/\r?\n/)
775
874
  ),
776
875
  ""
@@ -819,7 +918,9 @@ function formatCaseUsage(testCase) {
819
918
  parts.push(`${formatNumber(totalTokens)} tokens`);
820
919
  }
821
920
  if (toolCalls > 0) {
822
- parts.push(`${formatNumber(toolCalls)} tool${toolCalls === 1 ? "" : "s"}`);
921
+ parts.push(
922
+ `${formatNumber(toolCalls)} tool call${toolCalls === 1 ? "" : "s"}`
923
+ );
823
924
  }
824
925
  if (testCase.harness?.timingMs !== void 0) {
825
926
  parts.push(formatDuration(testCase.harness.timingMs));
@@ -992,16 +1093,22 @@ function mergeEvalReports(reports) {
992
1093
  minimum: Math.min(...scoredCases)
993
1094
  } : void 0,
994
1095
  usage: mergeUsage(reports.map((report) => report.usage)),
1096
+ judgeUsage: mergeUsage(reports.map((report) => report.judgeUsage)),
995
1097
  cases,
996
1098
  failures
997
1099
  };
998
1100
  }
999
1101
  function mergeUsage(usages) {
1102
+ const costs = usages.map((usage) => usage.costUsd).filter((cost) => cost !== void 0);
1103
+ const costComplete = !usages.some(
1104
+ (usage) => usage.costUsd === void 0 && (usage.totalTokens > 0 || usage.toolCalls > 0)
1105
+ );
1000
1106
  return {
1001
1107
  inputTokens: sum(usages, (usage) => usage.inputTokens),
1002
1108
  outputTokens: sum(usages, (usage) => usage.outputTokens),
1003
1109
  reasoningTokens: sum(usages, (usage) => usage.reasoningTokens),
1004
1110
  totalTokens: sum(usages, (usage) => usage.totalTokens),
1111
+ ...costComplete && costs.length > 0 ? { costUsd: costs.reduce((total, cost) => total + cost, 0) } : {},
1005
1112
  toolCalls: sum(usages, (usage) => usage.toolCalls)
1006
1113
  };
1007
1114
  }