@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/cli.mjs CHANGED
@@ -327,7 +327,8 @@ function collectEvalReport(input, options = {}) {
327
327
  const cases = workspace.cases.map(collectEvalCase);
328
328
  const failures = cases.filter((testCase) => testCase.status === "failed");
329
329
  const evalScores = cases.map((testCase) => testCase.eval?.avgScore).filter((score) => isFiniteNumber(score));
330
- const usage2 = sumUsage(cases);
330
+ const usage2 = sumAppUsage(cases);
331
+ const judgeUsage = sumJudgeUsage(cases);
331
332
  const durationMs = workspace.runs[0]?.durationMs;
332
333
  return {
333
334
  status: input.success && failures.length === 0 ? "passed" : "failed",
@@ -347,6 +348,7 @@ function collectEvalReport(input, options = {}) {
347
348
  minimum: Math.min(...evalScores)
348
349
  } : void 0,
349
350
  usage: usage2,
351
+ judgeUsage,
350
352
  cases,
351
353
  failures
352
354
  };
@@ -471,24 +473,67 @@ function stringifyReason(value) {
471
473
  }
472
474
  return typeof value === "string" ? value : stringifyValue(value, 4e3);
473
475
  }
474
- function sumUsage(cases) {
475
- const usage2 = {
476
+ function emptyUsage() {
477
+ return {
476
478
  inputTokens: 0,
477
479
  outputTokens: 0,
478
480
  reasoningTokens: 0,
479
481
  totalTokens: 0,
480
482
  toolCalls: 0
481
483
  };
484
+ }
485
+ function addRunUsage(total, usage2) {
486
+ total.inputTokens += usage2?.inputTokens ?? 0;
487
+ total.outputTokens += usage2?.outputTokens ?? 0;
488
+ total.reasoningTokens += usage2?.reasoningTokens ?? 0;
489
+ total.totalTokens += usage2?.totalTokens ?? (usage2?.inputTokens ?? 0) + (usage2?.outputTokens ?? 0) + (usage2?.reasoningTokens ?? 0);
490
+ if (usage2?.costUsd !== void 0) {
491
+ total.costUsd = (total.costUsd ?? 0) + usage2.costUsd;
492
+ }
493
+ total.toolCalls += usage2?.toolCalls ?? 0;
494
+ }
495
+ function sumAppUsage(cases) {
496
+ const usage2 = emptyUsage();
497
+ const runUsages = cases.map(appUsageForCase).filter((item) => item !== void 0);
498
+ for (const runUsage of runUsages) {
499
+ addRunUsage(usage2, runUsage);
500
+ }
501
+ omitPartialCost(usage2, runUsages);
502
+ return usage2;
503
+ }
504
+ function appUsageForCase(testCase) {
505
+ const usage2 = testCase.harness?.usage;
506
+ const effectiveToolCalls = toolCallCount(testCase);
507
+ if (!usage2 && effectiveToolCalls === 0) {
508
+ return void 0;
509
+ }
510
+ return {
511
+ ...usage2,
512
+ ...effectiveToolCalls > 0 ? { toolCalls: effectiveToolCalls } : {}
513
+ };
514
+ }
515
+ function sumJudgeUsage(cases) {
516
+ const usage2 = emptyUsage();
517
+ const runUsages = [];
482
518
  for (const testCase of cases) {
483
- const caseUsage = testCase.harness?.usage;
484
- usage2.inputTokens += caseUsage?.inputTokens ?? 0;
485
- usage2.outputTokens += caseUsage?.outputTokens ?? 0;
486
- usage2.reasoningTokens += caseUsage?.reasoningTokens ?? 0;
487
- usage2.totalTokens += caseUsage?.totalTokens ?? (caseUsage?.inputTokens ?? 0) + (caseUsage?.outputTokens ?? 0) + (caseUsage?.reasoningTokens ?? 0);
488
- usage2.toolCalls += toolCallCount(testCase);
519
+ for (const score of testCase.eval?.scores ?? []) {
520
+ for (const run of score.judgeRuns ?? []) {
521
+ runUsages.push(run.usage);
522
+ addRunUsage(usage2, run.usage);
523
+ }
524
+ }
489
525
  }
526
+ omitPartialCost(usage2, runUsages);
490
527
  return usage2;
491
528
  }
529
+ function omitPartialCost(total, usages) {
530
+ if (usages.some(hasUsageWithoutCost)) {
531
+ total.costUsd = void 0;
532
+ }
533
+ }
534
+ function hasUsageWithoutCost(usage2) {
535
+ return usage2?.costUsd === void 0 && ((usage2?.totalTokens ?? (usage2?.inputTokens ?? 0) + (usage2?.outputTokens ?? 0) + (usage2?.reasoningTokens ?? 0)) > 0 || (usage2?.toolCalls ?? 0) > 0);
536
+ }
492
537
  function toolCallCount(testCase) {
493
538
  const usageToolCalls = testCase.harness?.usage?.toolCalls;
494
539
  if (usageToolCalls !== void 0) {
@@ -525,8 +570,8 @@ function evaluateEvalGate(report, policy = {}) {
525
570
  status: "failed",
526
571
  enforced: true,
527
572
  passRate,
528
- title: "Eval report hard failure",
529
- message: `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}; ${counts}`
573
+ title: "Eval run failed",
574
+ message: `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases; ${counts}`
530
575
  };
531
576
  }
532
577
  if (report.totals.evalTotal === 0) {
@@ -535,7 +580,7 @@ function evaluateEvalGate(report, policy = {}) {
535
580
  status: "failed",
536
581
  enforced: true,
537
582
  passRate: null,
538
- title: "Eval report hard failure",
583
+ title: "Eval run failed",
539
584
  message: "no eval cases were reported"
540
585
  };
541
586
  }
@@ -545,8 +590,8 @@ function evaluateEvalGate(report, policy = {}) {
545
590
  status: "failed",
546
591
  enforced: true,
547
592
  passRate,
548
- title: "Eval report hard failure",
549
- message: `vitest run failed without counted test failures; ${counts}`
593
+ title: "Eval run failed",
594
+ message: `Vitest failed without reporting a failed test; ${counts}`
550
595
  };
551
596
  }
552
597
  if (minPassRate !== void 0 && (passRate === null || passRate + Number.EPSILON < minPassRate)) {
@@ -556,7 +601,7 @@ function evaluateEvalGate(report, policy = {}) {
556
601
  enforced: true,
557
602
  passRate,
558
603
  title: `Eval pass rate ${formatPercent(passRate)} \u2014 required ${formatPercent(minPassRate)}`,
559
- message: `eval pass rate below floor: ${counts}; required >= ${formatPercent(minPassRate)}`
604
+ message: `pass rate is below the minimum: ${counts}; minimum ${formatPercent(minPassRate)}`
560
605
  };
561
606
  }
562
607
  if (minScoreAverage !== void 0) {
@@ -567,8 +612,8 @@ function evaluateEvalGate(report, policy = {}) {
567
612
  status: "failed",
568
613
  enforced: true,
569
614
  passRate,
570
- title: "Eval score gate failed",
571
- message: `no score average available; required avg score >= ${formatScore(minScoreAverage)}`
615
+ title: "Average score unavailable",
616
+ message: `no average score was reported; minimum ${formatScore(minScoreAverage)}`
572
617
  };
573
618
  }
574
619
  if (average + Number.EPSILON < minScoreAverage) {
@@ -577,8 +622,8 @@ function evaluateEvalGate(report, policy = {}) {
577
622
  status: "failed",
578
623
  enforced: true,
579
624
  passRate,
580
- title: `Avg score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
581
- message: `avg score below floor: ${counts}; required avg score >= ${formatScore(minScoreAverage)}`
625
+ title: `Average score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
626
+ message: `average score is below the minimum: ${counts}; minimum ${formatScore(minScoreAverage)}`
582
627
  };
583
628
  }
584
629
  }
@@ -588,7 +633,7 @@ function evaluateEvalGate(report, policy = {}) {
588
633
  enforced: true,
589
634
  passRate,
590
635
  title: enforcedPassTitle(report, passRate, minPassRate, minScoreAverage),
591
- message: `eval gate passed: ${counts}${formatFloorSuffix(minPassRate, minScoreAverage)}`
636
+ message: `requirements met: ${counts}${formatMinimumSuffix(minPassRate, minScoreAverage)}`
592
637
  };
593
638
  }
594
639
  function computePassRate(report) {
@@ -621,7 +666,7 @@ function resolveMinPassRate(policy) {
621
666
  }
622
667
  function defaultCheckTitle(report) {
623
668
  if (report.failures.length === 0 && report.status === "passed") {
624
- return "No eval failures";
669
+ return "No eval cases failed";
625
670
  }
626
671
  if (report.failures.length === 0) {
627
672
  return "Vitest run failed";
@@ -630,10 +675,10 @@ function defaultCheckTitle(report) {
630
675
  }
631
676
  function enforcedPassTitle(report, passRate, minPassRate, minScoreAverage) {
632
677
  if (minPassRate !== void 0) {
633
- return `Eval pass rate ${formatPercent(passRate)} \u2014 floor ${formatPercent(minPassRate)}`;
678
+ return `Eval pass rate ${formatPercent(passRate)} \u2014 minimum ${formatPercent(minPassRate)}`;
634
679
  }
635
680
  if (minScoreAverage !== void 0) {
636
- return `Avg score ${formatScore(report.score?.average)} \u2014 floor ${formatScore(minScoreAverage)}`;
681
+ return `Average score ${formatScore(report.score?.average)} \u2014 minimum ${formatScore(minScoreAverage)}`;
637
682
  }
638
683
  return defaultCheckTitle(report);
639
684
  }
@@ -642,15 +687,15 @@ function formatEvalCounts(report, passRate) {
642
687
  const passRateText = passRate === null ? "n/a" : formatPercent(passRate);
643
688
  return `${formatNumber(report.totals.evalPassed)}/${formatNumber(
644
689
  report.totals.evalTotal
645
- )} passed (${passRateText}), avg score ${scoreText}`;
690
+ )} passed (${passRateText}), average score ${scoreText}`;
646
691
  }
647
- function formatFloorSuffix(minPassRate, minScoreAverage) {
692
+ function formatMinimumSuffix(minPassRate, minScoreAverage) {
648
693
  const parts = [];
649
694
  if (minPassRate !== void 0) {
650
- parts.push(`pass rate floor ${formatPercent(minPassRate)}`);
695
+ parts.push(`minimum pass rate ${formatPercent(minPassRate)}`);
651
696
  }
652
697
  if (minScoreAverage !== void 0) {
653
- parts.push(`avg score floor ${formatScore(minScoreAverage)}`);
698
+ parts.push(`minimum average score ${formatScore(minScoreAverage)}`);
654
699
  }
655
700
  return parts.length === 0 ? "" : `; ${parts.join(", ")}`;
656
701
  }
@@ -685,23 +730,23 @@ function renderJobSummary(report, options = {}) {
685
730
  ""
686
731
  ];
687
732
  if (report.failures.length > 0) {
688
- const failureHeading = options.gate?.ok === true ? "### Quality Misses" : "### Failures";
733
+ const failureHeading = options.gate?.ok === true ? "### Cases Below Target" : "### Failures";
689
734
  lines.push(failureHeading, "");
690
735
  failures.forEach((testCase, index) => {
691
736
  lines.push(...renderFailureDetails(testCase, index + 1, options), "");
692
737
  });
693
738
  if (report.failures.length > failures.length) {
694
- const omittedLabel = options.gate?.ok === true ? "quality misses" : "failures";
739
+ const omittedLabel = options.gate?.ok === true ? "cases below target" : "failures";
695
740
  lines.push(
696
741
  `${report.failures.length - failures.length} more ${omittedLabel} omitted from this summary.`,
697
742
  ""
698
743
  );
699
744
  }
700
745
  } else if (report.totals.evalTotal > 0) {
701
- lines.push("### Failures", "", "No eval failures.", "");
746
+ lines.push("### Failures", "", "No eval cases failed.", "");
702
747
  }
703
748
  if (report.totals.evalTotal === 0) {
704
- lines.push("No eval metadata was found in the Vitest JSON report.", "");
749
+ lines.push("No eval results were found in the Vitest JSON report.", "");
705
750
  }
706
751
  return `${lines.join("\n")}
707
752
  `;
@@ -711,9 +756,9 @@ function formatCountLine(passed, failed, total) {
711
756
  }
712
757
  function renderSummaryTable(report, nonEvalFailures, gate) {
713
758
  const rows = [
714
- ["Status", gate?.status ?? report.status],
759
+ ["Status", capitalize(gate?.status ?? report.status)],
715
760
  [
716
- "Evals",
761
+ "Eval cases",
717
762
  formatCountLine(
718
763
  report.totals.evalPassed,
719
764
  report.totals.evalFailed,
@@ -722,23 +767,31 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
722
767
  ]
723
768
  ];
724
769
  if (gate?.passRate !== void 0 && gate.passRate !== null) {
725
- rows.push(["Pass Rate", formatPercent(gate.passRate)]);
770
+ rows.push(["Pass rate", formatPercent(gate.passRate)]);
726
771
  } else if (report.totals.evalTotal > 0) {
727
772
  rows.push([
728
- "Pass Rate",
773
+ "Pass rate",
729
774
  formatPercent(report.totals.evalPassed / report.totals.evalTotal)
730
775
  ]);
731
776
  }
732
777
  if (report.score) {
733
778
  rows.push(["Score", formatScoreSummary(report.score)]);
734
779
  }
780
+ if (hasUsage(report.usage) || hasUsage(report.judgeUsage)) {
781
+ rows.push(["App usage", formatUsage(report.usage)]);
782
+ rows.push(["Judge usage", formatUsage(report.judgeUsage)]);
783
+ rows.push([
784
+ "Total usage",
785
+ formatUsage(sumUsage(report.usage, report.judgeUsage))
786
+ ]);
787
+ }
735
788
  if (gate?.enforced) {
736
- rows.push(["Gate", gate.message]);
789
+ rows.push(["Requirements", gate.message]);
737
790
  }
738
791
  if (nonEvalFailures > 0) {
739
792
  rows.push([
740
- "Other Failures",
741
- `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}`
793
+ "Other test failures",
794
+ `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases`
742
795
  ]);
743
796
  }
744
797
  rows.push(["Duration", formatDuration(report.durationMs)]);
@@ -751,7 +804,53 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
751
804
  ];
752
805
  }
753
806
  function formatScoreSummary(score) {
754
- return `avg ${formatScore(score.average)}${score.minimum === void 0 ? "" : `, min ${formatScore(score.minimum)}`}`;
807
+ return `average ${formatScore(score.average)}${score.minimum === void 0 ? "" : `, lowest ${formatScore(score.minimum)}`}`;
808
+ }
809
+ function capitalize(value) {
810
+ return value.charAt(0).toUpperCase() + value.slice(1);
811
+ }
812
+ function hasUsage(usage2) {
813
+ return usage2.totalTokens > 0 || usage2.costUsd !== void 0 || usage2.toolCalls > 0;
814
+ }
815
+ function sumUsage(app, judge) {
816
+ const appCostKnown = !hasUsageWithoutCost2(app);
817
+ const judgeCostKnown = !hasUsageWithoutCost2(judge);
818
+ return {
819
+ inputTokens: app.inputTokens + judge.inputTokens,
820
+ outputTokens: app.outputTokens + judge.outputTokens,
821
+ reasoningTokens: app.reasoningTokens + judge.reasoningTokens,
822
+ totalTokens: app.totalTokens + judge.totalTokens,
823
+ ...appCostKnown && judgeCostKnown ? { costUsd: (app.costUsd ?? 0) + (judge.costUsd ?? 0) } : {},
824
+ toolCalls: app.toolCalls + judge.toolCalls
825
+ };
826
+ }
827
+ function hasUsageWithoutCost2(usage2) {
828
+ return usage2.costUsd === void 0 && (usage2.totalTokens > 0 || usage2.toolCalls > 0);
829
+ }
830
+ function formatUsage(usage2) {
831
+ const parts = [];
832
+ if (usage2.totalTokens > 0) {
833
+ parts.push(`${formatNumber(usage2.totalTokens)} tokens`);
834
+ }
835
+ if (usage2.costUsd !== void 0) {
836
+ parts.push(
837
+ usage2.costUsd.toLocaleString("en-US", {
838
+ style: "currency",
839
+ currency: "USD",
840
+ minimumFractionDigits: 2,
841
+ maximumFractionDigits: 6
842
+ })
843
+ );
844
+ }
845
+ if (hasUsageWithoutCost2(usage2)) {
846
+ parts.push("cost unavailable");
847
+ }
848
+ if (usage2.toolCalls > 0) {
849
+ parts.push(
850
+ `${formatNumber(usage2.toolCalls)} tool call${usage2.toolCalls === 1 ? "" : "s"}`
851
+ );
852
+ }
853
+ return parts.join(", ") || "none";
755
854
  }
756
855
  function escapeTableCell(value) {
757
856
  return value.replace(/\r?\n/g, " ").replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
@@ -831,7 +930,7 @@ function renderFailureBlock(testCase, {
831
930
  ["Case", `${number}. ${testCase.displayName}`],
832
931
  ["Status", testCase.status],
833
932
  ["Location", formatLocation(testCase.displayFile, testCase.location)],
834
- ["Harness", testCase.harness?.name ?? "n/a"],
933
+ ["App runner", testCase.harness?.name ?? "n/a"],
835
934
  ["Score", formatScore(failure?.score ?? testCase.eval?.avgScore)],
836
935
  ["Judge", failure?.judgeName ?? "n/a"]
837
936
  ];
@@ -869,7 +968,7 @@ function renderFailureBlock(testCase, {
869
968
  if (finalOutput !== void 0) {
870
969
  lines.push(
871
970
  ...renderAsciiSection(
872
- "Final Output",
971
+ "Output",
873
972
  stringifyValue(finalOutput, maxOutputChars).split(/\r?\n/)
874
973
  ),
875
974
  ""
@@ -897,7 +996,7 @@ function renderFailureBlock(testCase, {
897
996
  if (testCase.harness?.errors.length) {
898
997
  lines.push(
899
998
  ...renderAsciiSection(
900
- "Harness Errors",
999
+ "App errors",
901
1000
  stringifyValue(testCase.harness.errors, maxReasonChars).split(/\r?\n/)
902
1001
  ),
903
1002
  ""
@@ -946,7 +1045,9 @@ function formatCaseUsage(testCase) {
946
1045
  parts.push(`${formatNumber(totalTokens)} tokens`);
947
1046
  }
948
1047
  if (toolCalls > 0) {
949
- parts.push(`${formatNumber(toolCalls)} tool${toolCalls === 1 ? "" : "s"}`);
1048
+ parts.push(
1049
+ `${formatNumber(toolCalls)} tool call${toolCalls === 1 ? "" : "s"}`
1050
+ );
950
1051
  }
951
1052
  if (testCase.harness?.timingMs !== void 0) {
952
1053
  parts.push(formatDuration(testCase.harness.timingMs));
@@ -1112,16 +1213,22 @@ function mergeEvalReports(reports) {
1112
1213
  minimum: Math.min(...scoredCases)
1113
1214
  } : void 0,
1114
1215
  usage: mergeUsage(reports.map((report) => report.usage)),
1216
+ judgeUsage: mergeUsage(reports.map((report) => report.judgeUsage)),
1115
1217
  cases,
1116
1218
  failures
1117
1219
  };
1118
1220
  }
1119
1221
  function mergeUsage(usages) {
1222
+ const costs = usages.map((usage2) => usage2.costUsd).filter((cost) => cost !== void 0);
1223
+ const costComplete = !usages.some(
1224
+ (usage2) => usage2.costUsd === void 0 && (usage2.totalTokens > 0 || usage2.toolCalls > 0)
1225
+ );
1120
1226
  return {
1121
1227
  inputTokens: sum(usages, (usage2) => usage2.inputTokens),
1122
1228
  outputTokens: sum(usages, (usage2) => usage2.outputTokens),
1123
1229
  reasoningTokens: sum(usages, (usage2) => usage2.reasoningTokens),
1124
1230
  totalTokens: sum(usages, (usage2) => usage2.totalTokens),
1231
+ ...costComplete && costs.length > 0 ? { costUsd: costs.reduce((total, cost) => total + cost, 0) } : {},
1125
1232
  toolCalls: sum(usages, (usage2) => usage2.toolCalls)
1126
1233
  };
1127
1234
  }