@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.js CHANGED
@@ -323,7 +323,8 @@ function collectEvalReport(input, options = {}) {
323
323
  const cases = workspace.cases.map(collectEvalCase);
324
324
  const failures = cases.filter((testCase) => testCase.status === "failed");
325
325
  const evalScores = cases.map((testCase) => testCase.eval?.avgScore).filter((score) => isFiniteNumber(score));
326
- const usage2 = sumUsage(cases);
326
+ const usage2 = sumAppUsage(cases);
327
+ const judgeUsage = sumJudgeUsage(cases);
327
328
  const durationMs = workspace.runs[0]?.durationMs;
328
329
  return {
329
330
  status: input.success && failures.length === 0 ? "passed" : "failed",
@@ -343,6 +344,7 @@ function collectEvalReport(input, options = {}) {
343
344
  minimum: Math.min(...evalScores)
344
345
  } : void 0,
345
346
  usage: usage2,
347
+ judgeUsage,
346
348
  cases,
347
349
  failures
348
350
  };
@@ -467,24 +469,67 @@ function stringifyReason(value) {
467
469
  }
468
470
  return typeof value === "string" ? value : stringifyValue(value, 4e3);
469
471
  }
470
- function sumUsage(cases) {
471
- const usage2 = {
472
+ function emptyUsage() {
473
+ return {
472
474
  inputTokens: 0,
473
475
  outputTokens: 0,
474
476
  reasoningTokens: 0,
475
477
  totalTokens: 0,
476
478
  toolCalls: 0
477
479
  };
480
+ }
481
+ function addRunUsage(total, usage2) {
482
+ total.inputTokens += usage2?.inputTokens ?? 0;
483
+ total.outputTokens += usage2?.outputTokens ?? 0;
484
+ total.reasoningTokens += usage2?.reasoningTokens ?? 0;
485
+ total.totalTokens += usage2?.totalTokens ?? (usage2?.inputTokens ?? 0) + (usage2?.outputTokens ?? 0) + (usage2?.reasoningTokens ?? 0);
486
+ if (usage2?.costUsd !== void 0) {
487
+ total.costUsd = (total.costUsd ?? 0) + usage2.costUsd;
488
+ }
489
+ total.toolCalls += usage2?.toolCalls ?? 0;
490
+ }
491
+ function sumAppUsage(cases) {
492
+ const usage2 = emptyUsage();
493
+ const runUsages = cases.map(appUsageForCase).filter((item) => item !== void 0);
494
+ for (const runUsage of runUsages) {
495
+ addRunUsage(usage2, runUsage);
496
+ }
497
+ omitPartialCost(usage2, runUsages);
498
+ return usage2;
499
+ }
500
+ function appUsageForCase(testCase) {
501
+ const usage2 = testCase.harness?.usage;
502
+ const effectiveToolCalls = toolCallCount(testCase);
503
+ if (!usage2 && effectiveToolCalls === 0) {
504
+ return void 0;
505
+ }
506
+ return {
507
+ ...usage2,
508
+ ...effectiveToolCalls > 0 ? { toolCalls: effectiveToolCalls } : {}
509
+ };
510
+ }
511
+ function sumJudgeUsage(cases) {
512
+ const usage2 = emptyUsage();
513
+ const runUsages = [];
478
514
  for (const testCase of cases) {
479
- const caseUsage = testCase.harness?.usage;
480
- usage2.inputTokens += caseUsage?.inputTokens ?? 0;
481
- usage2.outputTokens += caseUsage?.outputTokens ?? 0;
482
- usage2.reasoningTokens += caseUsage?.reasoningTokens ?? 0;
483
- usage2.totalTokens += caseUsage?.totalTokens ?? (caseUsage?.inputTokens ?? 0) + (caseUsage?.outputTokens ?? 0) + (caseUsage?.reasoningTokens ?? 0);
484
- usage2.toolCalls += toolCallCount(testCase);
515
+ for (const score of testCase.eval?.scores ?? []) {
516
+ for (const run of score.judgeRuns ?? []) {
517
+ runUsages.push(run.usage);
518
+ addRunUsage(usage2, run.usage);
519
+ }
520
+ }
485
521
  }
522
+ omitPartialCost(usage2, runUsages);
486
523
  return usage2;
487
524
  }
525
+ function omitPartialCost(total, usages) {
526
+ if (usages.some(hasUsageWithoutCost)) {
527
+ total.costUsd = void 0;
528
+ }
529
+ }
530
+ function hasUsageWithoutCost(usage2) {
531
+ return usage2?.costUsd === void 0 && ((usage2?.totalTokens ?? (usage2?.inputTokens ?? 0) + (usage2?.outputTokens ?? 0) + (usage2?.reasoningTokens ?? 0)) > 0 || (usage2?.toolCalls ?? 0) > 0);
532
+ }
488
533
  function toolCallCount(testCase) {
489
534
  const usageToolCalls = testCase.harness?.usage?.toolCalls;
490
535
  if (usageToolCalls !== void 0) {
@@ -521,8 +566,8 @@ function evaluateEvalGate(report, policy = {}) {
521
566
  status: "failed",
522
567
  enforced: true,
523
568
  passRate,
524
- title: "Eval report hard failure",
525
- message: `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}; ${counts}`
569
+ title: "Eval run failed",
570
+ message: `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases; ${counts}`
526
571
  };
527
572
  }
528
573
  if (report.totals.evalTotal === 0) {
@@ -531,7 +576,7 @@ function evaluateEvalGate(report, policy = {}) {
531
576
  status: "failed",
532
577
  enforced: true,
533
578
  passRate: null,
534
- title: "Eval report hard failure",
579
+ title: "Eval run failed",
535
580
  message: "no eval cases were reported"
536
581
  };
537
582
  }
@@ -541,8 +586,8 @@ function evaluateEvalGate(report, policy = {}) {
541
586
  status: "failed",
542
587
  enforced: true,
543
588
  passRate,
544
- title: "Eval report hard failure",
545
- message: `vitest run failed without counted test failures; ${counts}`
589
+ title: "Eval run failed",
590
+ message: `Vitest failed without reporting a failed test; ${counts}`
546
591
  };
547
592
  }
548
593
  if (minPassRate !== void 0 && (passRate === null || passRate + Number.EPSILON < minPassRate)) {
@@ -552,7 +597,7 @@ function evaluateEvalGate(report, policy = {}) {
552
597
  enforced: true,
553
598
  passRate,
554
599
  title: `Eval pass rate ${formatPercent(passRate)} \u2014 required ${formatPercent(minPassRate)}`,
555
- message: `eval pass rate below floor: ${counts}; required >= ${formatPercent(minPassRate)}`
600
+ message: `pass rate is below the minimum: ${counts}; minimum ${formatPercent(minPassRate)}`
556
601
  };
557
602
  }
558
603
  if (minScoreAverage !== void 0) {
@@ -563,8 +608,8 @@ function evaluateEvalGate(report, policy = {}) {
563
608
  status: "failed",
564
609
  enforced: true,
565
610
  passRate,
566
- title: "Eval score gate failed",
567
- message: `no score average available; required avg score >= ${formatScore(minScoreAverage)}`
611
+ title: "Average score unavailable",
612
+ message: `no average score was reported; minimum ${formatScore(minScoreAverage)}`
568
613
  };
569
614
  }
570
615
  if (average + Number.EPSILON < minScoreAverage) {
@@ -573,8 +618,8 @@ function evaluateEvalGate(report, policy = {}) {
573
618
  status: "failed",
574
619
  enforced: true,
575
620
  passRate,
576
- title: `Avg score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
577
- message: `avg score below floor: ${counts}; required avg score >= ${formatScore(minScoreAverage)}`
621
+ title: `Average score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
622
+ message: `average score is below the minimum: ${counts}; minimum ${formatScore(minScoreAverage)}`
578
623
  };
579
624
  }
580
625
  }
@@ -584,7 +629,7 @@ function evaluateEvalGate(report, policy = {}) {
584
629
  enforced: true,
585
630
  passRate,
586
631
  title: enforcedPassTitle(report, passRate, minPassRate, minScoreAverage),
587
- message: `eval gate passed: ${counts}${formatFloorSuffix(minPassRate, minScoreAverage)}`
632
+ message: `requirements met: ${counts}${formatMinimumSuffix(minPassRate, minScoreAverage)}`
588
633
  };
589
634
  }
590
635
  function computePassRate(report) {
@@ -617,7 +662,7 @@ function resolveMinPassRate(policy) {
617
662
  }
618
663
  function defaultCheckTitle(report) {
619
664
  if (report.failures.length === 0 && report.status === "passed") {
620
- return "No eval failures";
665
+ return "No eval cases failed";
621
666
  }
622
667
  if (report.failures.length === 0) {
623
668
  return "Vitest run failed";
@@ -626,10 +671,10 @@ function defaultCheckTitle(report) {
626
671
  }
627
672
  function enforcedPassTitle(report, passRate, minPassRate, minScoreAverage) {
628
673
  if (minPassRate !== void 0) {
629
- return `Eval pass rate ${formatPercent(passRate)} \u2014 floor ${formatPercent(minPassRate)}`;
674
+ return `Eval pass rate ${formatPercent(passRate)} \u2014 minimum ${formatPercent(minPassRate)}`;
630
675
  }
631
676
  if (minScoreAverage !== void 0) {
632
- return `Avg score ${formatScore(report.score?.average)} \u2014 floor ${formatScore(minScoreAverage)}`;
677
+ return `Average score ${formatScore(report.score?.average)} \u2014 minimum ${formatScore(minScoreAverage)}`;
633
678
  }
634
679
  return defaultCheckTitle(report);
635
680
  }
@@ -638,15 +683,15 @@ function formatEvalCounts(report, passRate) {
638
683
  const passRateText = passRate === null ? "n/a" : formatPercent(passRate);
639
684
  return `${formatNumber(report.totals.evalPassed)}/${formatNumber(
640
685
  report.totals.evalTotal
641
- )} passed (${passRateText}), avg score ${scoreText}`;
686
+ )} passed (${passRateText}), average score ${scoreText}`;
642
687
  }
643
- function formatFloorSuffix(minPassRate, minScoreAverage) {
688
+ function formatMinimumSuffix(minPassRate, minScoreAverage) {
644
689
  const parts = [];
645
690
  if (minPassRate !== void 0) {
646
- parts.push(`pass rate floor ${formatPercent(minPassRate)}`);
691
+ parts.push(`minimum pass rate ${formatPercent(minPassRate)}`);
647
692
  }
648
693
  if (minScoreAverage !== void 0) {
649
- parts.push(`avg score floor ${formatScore(minScoreAverage)}`);
694
+ parts.push(`minimum average score ${formatScore(minScoreAverage)}`);
650
695
  }
651
696
  return parts.length === 0 ? "" : `; ${parts.join(", ")}`;
652
697
  }
@@ -681,23 +726,23 @@ function renderJobSummary(report, options = {}) {
681
726
  ""
682
727
  ];
683
728
  if (report.failures.length > 0) {
684
- const failureHeading = options.gate?.ok === true ? "### Quality Misses" : "### Failures";
729
+ const failureHeading = options.gate?.ok === true ? "### Cases Below Target" : "### Failures";
685
730
  lines.push(failureHeading, "");
686
731
  failures.forEach((testCase, index) => {
687
732
  lines.push(...renderFailureDetails(testCase, index + 1, options), "");
688
733
  });
689
734
  if (report.failures.length > failures.length) {
690
- const omittedLabel = options.gate?.ok === true ? "quality misses" : "failures";
735
+ const omittedLabel = options.gate?.ok === true ? "cases below target" : "failures";
691
736
  lines.push(
692
737
  `${report.failures.length - failures.length} more ${omittedLabel} omitted from this summary.`,
693
738
  ""
694
739
  );
695
740
  }
696
741
  } else if (report.totals.evalTotal > 0) {
697
- lines.push("### Failures", "", "No eval failures.", "");
742
+ lines.push("### Failures", "", "No eval cases failed.", "");
698
743
  }
699
744
  if (report.totals.evalTotal === 0) {
700
- lines.push("No eval metadata was found in the Vitest JSON report.", "");
745
+ lines.push("No eval results were found in the Vitest JSON report.", "");
701
746
  }
702
747
  return `${lines.join("\n")}
703
748
  `;
@@ -707,9 +752,9 @@ function formatCountLine(passed, failed, total) {
707
752
  }
708
753
  function renderSummaryTable(report, nonEvalFailures, gate) {
709
754
  const rows = [
710
- ["Status", gate?.status ?? report.status],
755
+ ["Status", capitalize(gate?.status ?? report.status)],
711
756
  [
712
- "Evals",
757
+ "Eval cases",
713
758
  formatCountLine(
714
759
  report.totals.evalPassed,
715
760
  report.totals.evalFailed,
@@ -718,23 +763,31 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
718
763
  ]
719
764
  ];
720
765
  if (gate?.passRate !== void 0 && gate.passRate !== null) {
721
- rows.push(["Pass Rate", formatPercent(gate.passRate)]);
766
+ rows.push(["Pass rate", formatPercent(gate.passRate)]);
722
767
  } else if (report.totals.evalTotal > 0) {
723
768
  rows.push([
724
- "Pass Rate",
769
+ "Pass rate",
725
770
  formatPercent(report.totals.evalPassed / report.totals.evalTotal)
726
771
  ]);
727
772
  }
728
773
  if (report.score) {
729
774
  rows.push(["Score", formatScoreSummary(report.score)]);
730
775
  }
776
+ if (hasUsage(report.usage) || hasUsage(report.judgeUsage)) {
777
+ rows.push(["App usage", formatUsage(report.usage)]);
778
+ rows.push(["Judge usage", formatUsage(report.judgeUsage)]);
779
+ rows.push([
780
+ "Total usage",
781
+ formatUsage(sumUsage(report.usage, report.judgeUsage))
782
+ ]);
783
+ }
731
784
  if (gate?.enforced) {
732
- rows.push(["Gate", gate.message]);
785
+ rows.push(["Requirements", gate.message]);
733
786
  }
734
787
  if (nonEvalFailures > 0) {
735
788
  rows.push([
736
- "Other Failures",
737
- `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}`
789
+ "Other test failures",
790
+ `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases`
738
791
  ]);
739
792
  }
740
793
  rows.push(["Duration", formatDuration(report.durationMs)]);
@@ -747,7 +800,53 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
747
800
  ];
748
801
  }
749
802
  function formatScoreSummary(score) {
750
- return `avg ${formatScore(score.average)}${score.minimum === void 0 ? "" : `, min ${formatScore(score.minimum)}`}`;
803
+ return `average ${formatScore(score.average)}${score.minimum === void 0 ? "" : `, lowest ${formatScore(score.minimum)}`}`;
804
+ }
805
+ function capitalize(value) {
806
+ return value.charAt(0).toUpperCase() + value.slice(1);
807
+ }
808
+ function hasUsage(usage2) {
809
+ return usage2.totalTokens > 0 || usage2.costUsd !== void 0 || usage2.toolCalls > 0;
810
+ }
811
+ function sumUsage(app, judge) {
812
+ const appCostKnown = !hasUsageWithoutCost2(app);
813
+ const judgeCostKnown = !hasUsageWithoutCost2(judge);
814
+ return {
815
+ inputTokens: app.inputTokens + judge.inputTokens,
816
+ outputTokens: app.outputTokens + judge.outputTokens,
817
+ reasoningTokens: app.reasoningTokens + judge.reasoningTokens,
818
+ totalTokens: app.totalTokens + judge.totalTokens,
819
+ ...appCostKnown && judgeCostKnown ? { costUsd: (app.costUsd ?? 0) + (judge.costUsd ?? 0) } : {},
820
+ toolCalls: app.toolCalls + judge.toolCalls
821
+ };
822
+ }
823
+ function hasUsageWithoutCost2(usage2) {
824
+ return usage2.costUsd === void 0 && (usage2.totalTokens > 0 || usage2.toolCalls > 0);
825
+ }
826
+ function formatUsage(usage2) {
827
+ const parts = [];
828
+ if (usage2.totalTokens > 0) {
829
+ parts.push(`${formatNumber(usage2.totalTokens)} tokens`);
830
+ }
831
+ if (usage2.costUsd !== void 0) {
832
+ parts.push(
833
+ usage2.costUsd.toLocaleString("en-US", {
834
+ style: "currency",
835
+ currency: "USD",
836
+ minimumFractionDigits: 2,
837
+ maximumFractionDigits: 6
838
+ })
839
+ );
840
+ }
841
+ if (hasUsageWithoutCost2(usage2)) {
842
+ parts.push("cost unavailable");
843
+ }
844
+ if (usage2.toolCalls > 0) {
845
+ parts.push(
846
+ `${formatNumber(usage2.toolCalls)} tool call${usage2.toolCalls === 1 ? "" : "s"}`
847
+ );
848
+ }
849
+ return parts.join(", ") || "none";
751
850
  }
752
851
  function escapeTableCell(value) {
753
852
  return value.replace(/\r?\n/g, " ").replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
@@ -827,7 +926,7 @@ function renderFailureBlock(testCase, {
827
926
  ["Case", `${number}. ${testCase.displayName}`],
828
927
  ["Status", testCase.status],
829
928
  ["Location", formatLocation(testCase.displayFile, testCase.location)],
830
- ["Harness", testCase.harness?.name ?? "n/a"],
929
+ ["App runner", testCase.harness?.name ?? "n/a"],
831
930
  ["Score", formatScore(failure?.score ?? testCase.eval?.avgScore)],
832
931
  ["Judge", failure?.judgeName ?? "n/a"]
833
932
  ];
@@ -865,7 +964,7 @@ function renderFailureBlock(testCase, {
865
964
  if (finalOutput !== void 0) {
866
965
  lines.push(
867
966
  ...renderAsciiSection(
868
- "Final Output",
967
+ "Output",
869
968
  stringifyValue(finalOutput, maxOutputChars).split(/\r?\n/)
870
969
  ),
871
970
  ""
@@ -893,7 +992,7 @@ function renderFailureBlock(testCase, {
893
992
  if (testCase.harness?.errors.length) {
894
993
  lines.push(
895
994
  ...renderAsciiSection(
896
- "Harness Errors",
995
+ "App errors",
897
996
  stringifyValue(testCase.harness.errors, maxReasonChars).split(/\r?\n/)
898
997
  ),
899
998
  ""
@@ -942,7 +1041,9 @@ function formatCaseUsage(testCase) {
942
1041
  parts.push(`${formatNumber(totalTokens)} tokens`);
943
1042
  }
944
1043
  if (toolCalls > 0) {
945
- parts.push(`${formatNumber(toolCalls)} tool${toolCalls === 1 ? "" : "s"}`);
1044
+ parts.push(
1045
+ `${formatNumber(toolCalls)} tool call${toolCalls === 1 ? "" : "s"}`
1046
+ );
946
1047
  }
947
1048
  if (testCase.harness?.timingMs !== void 0) {
948
1049
  parts.push(formatDuration(testCase.harness.timingMs));
@@ -1108,16 +1209,22 @@ function mergeEvalReports(reports) {
1108
1209
  minimum: Math.min(...scoredCases)
1109
1210
  } : void 0,
1110
1211
  usage: mergeUsage(reports.map((report) => report.usage)),
1212
+ judgeUsage: mergeUsage(reports.map((report) => report.judgeUsage)),
1111
1213
  cases,
1112
1214
  failures
1113
1215
  };
1114
1216
  }
1115
1217
  function mergeUsage(usages) {
1218
+ const costs = usages.map((usage2) => usage2.costUsd).filter((cost) => cost !== void 0);
1219
+ const costComplete = !usages.some(
1220
+ (usage2) => usage2.costUsd === void 0 && (usage2.totalTokens > 0 || usage2.toolCalls > 0)
1221
+ );
1116
1222
  return {
1117
1223
  inputTokens: sum(usages, (usage2) => usage2.inputTokens),
1118
1224
  outputTokens: sum(usages, (usage2) => usage2.outputTokens),
1119
1225
  reasoningTokens: sum(usages, (usage2) => usage2.reasoningTokens),
1120
1226
  totalTokens: sum(usages, (usage2) => usage2.totalTokens),
1227
+ ...costComplete && costs.length > 0 ? { costUsd: costs.reduce((total, cost) => total + cost, 0) } : {},
1121
1228
  toolCalls: sum(usages, (usage2) => usage2.toolCalls)
1122
1229
  };
1123
1230
  }