@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.js CHANGED
@@ -128,7 +128,8 @@ function collectEvalReport(input, options = {}) {
128
128
  const cases = workspace.cases.map(collectEvalCase);
129
129
  const failures = cases.filter((testCase) => testCase.status === "failed");
130
130
  const evalScores = cases.map((testCase) => testCase.eval?.avgScore).filter((score) => isFiniteNumber(score));
131
- const usage = sumUsage(cases);
131
+ const usage = sumAppUsage(cases);
132
+ const judgeUsage = sumJudgeUsage(cases);
132
133
  const durationMs = workspace.runs[0]?.durationMs;
133
134
  return {
134
135
  status: input.success && failures.length === 0 ? "passed" : "failed",
@@ -148,6 +149,7 @@ function collectEvalReport(input, options = {}) {
148
149
  minimum: Math.min(...evalScores)
149
150
  } : void 0,
150
151
  usage,
152
+ judgeUsage,
151
153
  cases,
152
154
  failures
153
155
  };
@@ -272,24 +274,67 @@ function stringifyReason(value) {
272
274
  }
273
275
  return typeof value === "string" ? value : stringifyValue(value, 4e3);
274
276
  }
275
- function sumUsage(cases) {
276
- const usage = {
277
+ function emptyUsage() {
278
+ return {
277
279
  inputTokens: 0,
278
280
  outputTokens: 0,
279
281
  reasoningTokens: 0,
280
282
  totalTokens: 0,
281
283
  toolCalls: 0
282
284
  };
285
+ }
286
+ function addRunUsage(total, usage) {
287
+ total.inputTokens += usage?.inputTokens ?? 0;
288
+ total.outputTokens += usage?.outputTokens ?? 0;
289
+ total.reasoningTokens += usage?.reasoningTokens ?? 0;
290
+ total.totalTokens += usage?.totalTokens ?? (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0) + (usage?.reasoningTokens ?? 0);
291
+ if (usage?.costUsd !== void 0) {
292
+ total.costUsd = (total.costUsd ?? 0) + usage.costUsd;
293
+ }
294
+ total.toolCalls += usage?.toolCalls ?? 0;
295
+ }
296
+ function sumAppUsage(cases) {
297
+ const usage = emptyUsage();
298
+ const runUsages = cases.map(appUsageForCase).filter((item) => item !== void 0);
299
+ for (const runUsage of runUsages) {
300
+ addRunUsage(usage, runUsage);
301
+ }
302
+ omitPartialCost(usage, runUsages);
303
+ return usage;
304
+ }
305
+ function appUsageForCase(testCase) {
306
+ const usage = testCase.harness?.usage;
307
+ const effectiveToolCalls = toolCallCount(testCase);
308
+ if (!usage && effectiveToolCalls === 0) {
309
+ return void 0;
310
+ }
311
+ return {
312
+ ...usage,
313
+ ...effectiveToolCalls > 0 ? { toolCalls: effectiveToolCalls } : {}
314
+ };
315
+ }
316
+ function sumJudgeUsage(cases) {
317
+ const usage = emptyUsage();
318
+ const runUsages = [];
283
319
  for (const testCase of cases) {
284
- const caseUsage = testCase.harness?.usage;
285
- usage.inputTokens += caseUsage?.inputTokens ?? 0;
286
- usage.outputTokens += caseUsage?.outputTokens ?? 0;
287
- usage.reasoningTokens += caseUsage?.reasoningTokens ?? 0;
288
- usage.totalTokens += caseUsage?.totalTokens ?? (caseUsage?.inputTokens ?? 0) + (caseUsage?.outputTokens ?? 0) + (caseUsage?.reasoningTokens ?? 0);
289
- usage.toolCalls += toolCallCount(testCase);
320
+ for (const score of testCase.eval?.scores ?? []) {
321
+ for (const run of score.judgeRuns ?? []) {
322
+ runUsages.push(run.usage);
323
+ addRunUsage(usage, run.usage);
324
+ }
325
+ }
290
326
  }
327
+ omitPartialCost(usage, runUsages);
291
328
  return usage;
292
329
  }
330
+ function omitPartialCost(total, usages) {
331
+ if (usages.some(hasUsageWithoutCost)) {
332
+ total.costUsd = void 0;
333
+ }
334
+ }
335
+ function hasUsageWithoutCost(usage) {
336
+ return usage?.costUsd === void 0 && ((usage?.totalTokens ?? (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0) + (usage?.reasoningTokens ?? 0)) > 0 || (usage?.toolCalls ?? 0) > 0);
337
+ }
293
338
  function toolCallCount(testCase) {
294
339
  const usageToolCalls = testCase.harness?.usage?.toolCalls;
295
340
  if (usageToolCalls !== void 0) {
@@ -433,8 +478,8 @@ function evaluateEvalGate(report, policy = {}) {
433
478
  status: "failed",
434
479
  enforced: true,
435
480
  passRate,
436
- title: "Eval report hard failure",
437
- message: `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}; ${counts}`
481
+ title: "Eval run failed",
482
+ message: `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases; ${counts}`
438
483
  };
439
484
  }
440
485
  if (report.totals.evalTotal === 0) {
@@ -443,7 +488,7 @@ function evaluateEvalGate(report, policy = {}) {
443
488
  status: "failed",
444
489
  enforced: true,
445
490
  passRate: null,
446
- title: "Eval report hard failure",
491
+ title: "Eval run failed",
447
492
  message: "no eval cases were reported"
448
493
  };
449
494
  }
@@ -453,8 +498,8 @@ function evaluateEvalGate(report, policy = {}) {
453
498
  status: "failed",
454
499
  enforced: true,
455
500
  passRate,
456
- title: "Eval report hard failure",
457
- message: `vitest run failed without counted test failures; ${counts}`
501
+ title: "Eval run failed",
502
+ message: `Vitest failed without reporting a failed test; ${counts}`
458
503
  };
459
504
  }
460
505
  if (minPassRate !== void 0 && (passRate === null || passRate + Number.EPSILON < minPassRate)) {
@@ -464,7 +509,7 @@ function evaluateEvalGate(report, policy = {}) {
464
509
  enforced: true,
465
510
  passRate,
466
511
  title: `Eval pass rate ${formatPercent(passRate)} \u2014 required ${formatPercent(minPassRate)}`,
467
- message: `eval pass rate below floor: ${counts}; required >= ${formatPercent(minPassRate)}`
512
+ message: `pass rate is below the minimum: ${counts}; minimum ${formatPercent(minPassRate)}`
468
513
  };
469
514
  }
470
515
  if (minScoreAverage !== void 0) {
@@ -475,8 +520,8 @@ function evaluateEvalGate(report, policy = {}) {
475
520
  status: "failed",
476
521
  enforced: true,
477
522
  passRate,
478
- title: "Eval score gate failed",
479
- message: `no score average available; required avg score >= ${formatScore(minScoreAverage)}`
523
+ title: "Average score unavailable",
524
+ message: `no average score was reported; minimum ${formatScore(minScoreAverage)}`
480
525
  };
481
526
  }
482
527
  if (average + Number.EPSILON < minScoreAverage) {
@@ -485,8 +530,8 @@ function evaluateEvalGate(report, policy = {}) {
485
530
  status: "failed",
486
531
  enforced: true,
487
532
  passRate,
488
- title: `Avg score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
489
- message: `avg score below floor: ${counts}; required avg score >= ${formatScore(minScoreAverage)}`
533
+ title: `Average score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
534
+ message: `average score is below the minimum: ${counts}; minimum ${formatScore(minScoreAverage)}`
490
535
  };
491
536
  }
492
537
  }
@@ -496,7 +541,7 @@ function evaluateEvalGate(report, policy = {}) {
496
541
  enforced: true,
497
542
  passRate,
498
543
  title: enforcedPassTitle(report, passRate, minPassRate, minScoreAverage),
499
- message: `eval gate passed: ${counts}${formatFloorSuffix(minPassRate, minScoreAverage)}`
544
+ message: `requirements met: ${counts}${formatMinimumSuffix(minPassRate, minScoreAverage)}`
500
545
  };
501
546
  }
502
547
  function computePassRate(report) {
@@ -529,7 +574,7 @@ function resolveMinPassRate(policy) {
529
574
  }
530
575
  function defaultCheckTitle(report) {
531
576
  if (report.failures.length === 0 && report.status === "passed") {
532
- return "No eval failures";
577
+ return "No eval cases failed";
533
578
  }
534
579
  if (report.failures.length === 0) {
535
580
  return "Vitest run failed";
@@ -538,10 +583,10 @@ function defaultCheckTitle(report) {
538
583
  }
539
584
  function enforcedPassTitle(report, passRate, minPassRate, minScoreAverage) {
540
585
  if (minPassRate !== void 0) {
541
- return `Eval pass rate ${formatPercent(passRate)} \u2014 floor ${formatPercent(minPassRate)}`;
586
+ return `Eval pass rate ${formatPercent(passRate)} \u2014 minimum ${formatPercent(minPassRate)}`;
542
587
  }
543
588
  if (minScoreAverage !== void 0) {
544
- return `Avg score ${formatScore(report.score?.average)} \u2014 floor ${formatScore(minScoreAverage)}`;
589
+ return `Average score ${formatScore(report.score?.average)} \u2014 minimum ${formatScore(minScoreAverage)}`;
545
590
  }
546
591
  return defaultCheckTitle(report);
547
592
  }
@@ -550,15 +595,15 @@ function formatEvalCounts(report, passRate) {
550
595
  const passRateText = passRate === null ? "n/a" : formatPercent(passRate);
551
596
  return `${formatNumber(report.totals.evalPassed)}/${formatNumber(
552
597
  report.totals.evalTotal
553
- )} passed (${passRateText}), avg score ${scoreText}`;
598
+ )} passed (${passRateText}), average score ${scoreText}`;
554
599
  }
555
- function formatFloorSuffix(minPassRate, minScoreAverage) {
600
+ function formatMinimumSuffix(minPassRate, minScoreAverage) {
556
601
  const parts = [];
557
602
  if (minPassRate !== void 0) {
558
- parts.push(`pass rate floor ${formatPercent(minPassRate)}`);
603
+ parts.push(`minimum pass rate ${formatPercent(minPassRate)}`);
559
604
  }
560
605
  if (minScoreAverage !== void 0) {
561
- parts.push(`avg score floor ${formatScore(minScoreAverage)}`);
606
+ parts.push(`minimum average score ${formatScore(minScoreAverage)}`);
562
607
  }
563
608
  return parts.length === 0 ? "" : `; ${parts.join(", ")}`;
564
609
  }
@@ -593,23 +638,23 @@ function renderJobSummary(report, options = {}) {
593
638
  ""
594
639
  ];
595
640
  if (report.failures.length > 0) {
596
- const failureHeading = options.gate?.ok === true ? "### Quality Misses" : "### Failures";
641
+ const failureHeading = options.gate?.ok === true ? "### Cases Below Target" : "### Failures";
597
642
  lines.push(failureHeading, "");
598
643
  failures.forEach((testCase, index) => {
599
644
  lines.push(...renderFailureDetails(testCase, index + 1, options), "");
600
645
  });
601
646
  if (report.failures.length > failures.length) {
602
- const omittedLabel = options.gate?.ok === true ? "quality misses" : "failures";
647
+ const omittedLabel = options.gate?.ok === true ? "cases below target" : "failures";
603
648
  lines.push(
604
649
  `${report.failures.length - failures.length} more ${omittedLabel} omitted from this summary.`,
605
650
  ""
606
651
  );
607
652
  }
608
653
  } else if (report.totals.evalTotal > 0) {
609
- lines.push("### Failures", "", "No eval failures.", "");
654
+ lines.push("### Failures", "", "No eval cases failed.", "");
610
655
  }
611
656
  if (report.totals.evalTotal === 0) {
612
- lines.push("No eval metadata was found in the Vitest JSON report.", "");
657
+ lines.push("No eval results were found in the Vitest JSON report.", "");
613
658
  }
614
659
  return `${lines.join("\n")}
615
660
  `;
@@ -619,9 +664,9 @@ function formatCountLine(passed, failed, total) {
619
664
  }
620
665
  function renderSummaryTable(report, nonEvalFailures, gate) {
621
666
  const rows = [
622
- ["Status", gate?.status ?? report.status],
667
+ ["Status", capitalize(gate?.status ?? report.status)],
623
668
  [
624
- "Evals",
669
+ "Eval cases",
625
670
  formatCountLine(
626
671
  report.totals.evalPassed,
627
672
  report.totals.evalFailed,
@@ -630,23 +675,31 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
630
675
  ]
631
676
  ];
632
677
  if (gate?.passRate !== void 0 && gate.passRate !== null) {
633
- rows.push(["Pass Rate", formatPercent(gate.passRate)]);
678
+ rows.push(["Pass rate", formatPercent(gate.passRate)]);
634
679
  } else if (report.totals.evalTotal > 0) {
635
680
  rows.push([
636
- "Pass Rate",
681
+ "Pass rate",
637
682
  formatPercent(report.totals.evalPassed / report.totals.evalTotal)
638
683
  ]);
639
684
  }
640
685
  if (report.score) {
641
686
  rows.push(["Score", formatScoreSummary(report.score)]);
642
687
  }
688
+ if (hasUsage(report.usage) || hasUsage(report.judgeUsage)) {
689
+ rows.push(["App usage", formatUsage(report.usage)]);
690
+ rows.push(["Judge usage", formatUsage(report.judgeUsage)]);
691
+ rows.push([
692
+ "Total usage",
693
+ formatUsage(sumUsage(report.usage, report.judgeUsage))
694
+ ]);
695
+ }
643
696
  if (gate?.enforced) {
644
- rows.push(["Gate", gate.message]);
697
+ rows.push(["Requirements", gate.message]);
645
698
  }
646
699
  if (nonEvalFailures > 0) {
647
700
  rows.push([
648
- "Other Failures",
649
- `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}`
701
+ "Other test failures",
702
+ `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases`
650
703
  ]);
651
704
  }
652
705
  rows.push(["Duration", formatDuration(report.durationMs)]);
@@ -659,7 +712,53 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
659
712
  ];
660
713
  }
661
714
  function formatScoreSummary(score) {
662
- return `avg ${formatScore(score.average)}${score.minimum === void 0 ? "" : `, min ${formatScore(score.minimum)}`}`;
715
+ return `average ${formatScore(score.average)}${score.minimum === void 0 ? "" : `, lowest ${formatScore(score.minimum)}`}`;
716
+ }
717
+ function capitalize(value) {
718
+ return value.charAt(0).toUpperCase() + value.slice(1);
719
+ }
720
+ function hasUsage(usage) {
721
+ return usage.totalTokens > 0 || usage.costUsd !== void 0 || usage.toolCalls > 0;
722
+ }
723
+ function sumUsage(app, judge) {
724
+ const appCostKnown = !hasUsageWithoutCost2(app);
725
+ const judgeCostKnown = !hasUsageWithoutCost2(judge);
726
+ return {
727
+ inputTokens: app.inputTokens + judge.inputTokens,
728
+ outputTokens: app.outputTokens + judge.outputTokens,
729
+ reasoningTokens: app.reasoningTokens + judge.reasoningTokens,
730
+ totalTokens: app.totalTokens + judge.totalTokens,
731
+ ...appCostKnown && judgeCostKnown ? { costUsd: (app.costUsd ?? 0) + (judge.costUsd ?? 0) } : {},
732
+ toolCalls: app.toolCalls + judge.toolCalls
733
+ };
734
+ }
735
+ function hasUsageWithoutCost2(usage) {
736
+ return usage.costUsd === void 0 && (usage.totalTokens > 0 || usage.toolCalls > 0);
737
+ }
738
+ function formatUsage(usage) {
739
+ const parts = [];
740
+ if (usage.totalTokens > 0) {
741
+ parts.push(`${formatNumber(usage.totalTokens)} tokens`);
742
+ }
743
+ if (usage.costUsd !== void 0) {
744
+ parts.push(
745
+ usage.costUsd.toLocaleString("en-US", {
746
+ style: "currency",
747
+ currency: "USD",
748
+ minimumFractionDigits: 2,
749
+ maximumFractionDigits: 6
750
+ })
751
+ );
752
+ }
753
+ if (hasUsageWithoutCost2(usage)) {
754
+ parts.push("cost unavailable");
755
+ }
756
+ if (usage.toolCalls > 0) {
757
+ parts.push(
758
+ `${formatNumber(usage.toolCalls)} tool call${usage.toolCalls === 1 ? "" : "s"}`
759
+ );
760
+ }
761
+ return parts.join(", ") || "none";
663
762
  }
664
763
  function escapeTableCell(value) {
665
764
  return value.replace(/\r?\n/g, " ").replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
@@ -739,7 +838,7 @@ function renderFailureBlock(testCase, {
739
838
  ["Case", `${number}. ${testCase.displayName}`],
740
839
  ["Status", testCase.status],
741
840
  ["Location", formatLocation(testCase.displayFile, testCase.location)],
742
- ["Harness", testCase.harness?.name ?? "n/a"],
841
+ ["App runner", testCase.harness?.name ?? "n/a"],
743
842
  ["Score", formatScore(failure?.score ?? testCase.eval?.avgScore)],
744
843
  ["Judge", failure?.judgeName ?? "n/a"]
745
844
  ];
@@ -777,7 +876,7 @@ function renderFailureBlock(testCase, {
777
876
  if (finalOutput !== void 0) {
778
877
  lines.push(
779
878
  ...renderAsciiSection(
780
- "Final Output",
879
+ "Output",
781
880
  stringifyValue(finalOutput, maxOutputChars).split(/\r?\n/)
782
881
  ),
783
882
  ""
@@ -805,7 +904,7 @@ function renderFailureBlock(testCase, {
805
904
  if (testCase.harness?.errors.length) {
806
905
  lines.push(
807
906
  ...renderAsciiSection(
808
- "Harness Errors",
907
+ "App errors",
809
908
  stringifyValue(testCase.harness.errors, maxReasonChars).split(/\r?\n/)
810
909
  ),
811
910
  ""
@@ -854,7 +953,9 @@ function formatCaseUsage(testCase) {
854
953
  parts.push(`${formatNumber(totalTokens)} tokens`);
855
954
  }
856
955
  if (toolCalls > 0) {
857
- parts.push(`${formatNumber(toolCalls)} tool${toolCalls === 1 ? "" : "s"}`);
956
+ parts.push(
957
+ `${formatNumber(toolCalls)} tool call${toolCalls === 1 ? "" : "s"}`
958
+ );
858
959
  }
859
960
  if (testCase.harness?.timingMs !== void 0) {
860
961
  parts.push(formatDuration(testCase.harness.timingMs));
@@ -1024,16 +1125,22 @@ function mergeEvalReports(reports) {
1024
1125
  minimum: Math.min(...scoredCases)
1025
1126
  } : void 0,
1026
1127
  usage: mergeUsage(reports.map((report) => report.usage)),
1128
+ judgeUsage: mergeUsage(reports.map((report) => report.judgeUsage)),
1027
1129
  cases,
1028
1130
  failures
1029
1131
  };
1030
1132
  }
1031
1133
  function mergeUsage(usages) {
1134
+ const costs = usages.map((usage) => usage.costUsd).filter((cost) => cost !== void 0);
1135
+ const costComplete = !usages.some(
1136
+ (usage) => usage.costUsd === void 0 && (usage.totalTokens > 0 || usage.toolCalls > 0)
1137
+ );
1032
1138
  return {
1033
1139
  inputTokens: sum(usages, (usage) => usage.inputTokens),
1034
1140
  outputTokens: sum(usages, (usage) => usage.outputTokens),
1035
1141
  reasoningTokens: sum(usages, (usage) => usage.reasoningTokens),
1036
1142
  totalTokens: sum(usages, (usage) => usage.totalTokens),
1143
+ ...costComplete && costs.length > 0 ? { costUsd: costs.reduce((total, cost) => total + cost, 0) } : {},
1037
1144
  toolCalls: sum(usages, (usage) => usage.toolCalls)
1038
1145
  };
1039
1146
  }