@vitest-evals/github-reporter 0.16.0 → 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.d.ts CHANGED
@@ -53,7 +53,10 @@ type EvalReport = {
53
53
  average: number;
54
54
  minimum?: number;
55
55
  };
56
- usage: Required<UsageSummary>;
56
+ /** Application and other upstream usage. */
57
+ usage: AggregatedUsageSummary;
58
+ /** Judge-model usage. */
59
+ judgeUsage: AggregatedUsageSummary;
57
60
  cases: EvalCase[];
58
61
  failures: EvalCase[];
59
62
  };
@@ -63,8 +66,11 @@ type UsageSummary = {
63
66
  outputTokens?: number;
64
67
  reasoningTokens?: number;
65
68
  totalTokens?: number;
69
+ costUsd?: number;
66
70
  toolCalls?: number;
67
71
  };
72
+ /** Aggregated usage; cost stays absent when no run reported one. */
73
+ type AggregatedUsageSummary = Required<Omit<UsageSummary, "costUsd">> & Pick<UsageSummary, "costUsd">;
68
74
  /** Tool-call summary shown in reporter output. */
69
75
  type ToolCallSummary = {
70
76
  name: string;
@@ -171,6 +177,8 @@ type PublishCheckRunOptions = SummaryOptions & {
171
177
  sha?: string;
172
178
  name?: string;
173
179
  apiUrl?: string;
180
+ detailsUrl?: string;
181
+ externalId?: string;
174
182
  checkRunId?: number;
175
183
  maxAnnotations?: number;
176
184
  /**
@@ -187,7 +195,26 @@ type PublishCheckRunResult = {
187
195
  status: "created" | "updated";
188
196
  id?: number;
189
197
  htmlUrl?: string;
198
+ sha?: string;
190
199
  };
200
+ /**
201
+ * Resolve the commit SHA a Check Run should attach to.
202
+ *
203
+ * On `pull_request`, `GITHUB_SHA` is the temporary merge commit. PR status and
204
+ * required checks attach to the head commit, so prefer an explicit head SHA
205
+ * (option/env or `pull_request.head.sha` from the event payload) first.
206
+ */
207
+ declare function resolveCheckSha(env?: NodeJS.ProcessEnv, options?: {
208
+ sha?: string;
209
+ eventPath?: string;
210
+ }): string | undefined;
211
+ /**
212
+ * Build a Check Run details URL that points back at the current workflow job
213
+ * (or run) when GitHub Actions env is present.
214
+ */
215
+ declare function resolveCheckDetailsUrl(env?: NodeJS.ProcessEnv, options?: {
216
+ detailsUrl?: string;
217
+ }): string | undefined;
191
218
  /** Publishes the eval report to a GitHub Check Run when configuration allows it. */
192
219
  declare function publishCheckRun(report: EvalReport, options?: PublishCheckRunOptions): Promise<PublishCheckRunResult>;
193
220
 
@@ -201,12 +228,21 @@ type PublishEvalReportOptions = SummaryOptions & EvalGatePolicy & {
201
228
  annotations?: boolean;
202
229
  checkRun?: boolean;
203
230
  failOnCheckError?: boolean;
231
+ /**
232
+ * When true, keep the workflow step green on a failed gate so the Check
233
+ * Run owns PR status instead of a canned job failure line. Defaults to
234
+ * true only when a Check Run is actually published; if publishing is
235
+ * skipped/fails, the step still exits non-zero on a failed gate.
236
+ */
237
+ softFail?: boolean;
204
238
  maxAnnotations?: number;
205
239
  checkRunId?: number;
206
240
  checkName?: string;
207
241
  token?: string;
208
242
  repository?: string;
209
243
  sha?: string;
244
+ detailsUrl?: string;
245
+ externalId?: string;
210
246
  warn?: (message: string) => void;
211
247
  };
212
248
  /** Result from publishing eval reports, including merged report data. */
@@ -215,8 +251,9 @@ type PublishEvalReportResult = {
215
251
  resultFiles: string[];
216
252
  gate: EvalGateResult;
217
253
  /**
218
- * True when an enforced gate rejects the report. Advisory (ungated) runs
219
- * never fail the step, matching `fail-on-failures: false` defaults.
254
+ * True when an enforced gate rejects the report and soft-fail did not keep
255
+ * the step green. Advisory (ungated) runs never fail the step, matching
256
+ * `fail-on-failures: false` defaults.
220
257
  */
221
258
  shouldFail: boolean;
222
259
  checkRun?: PublishCheckRunResult;
@@ -224,4 +261,4 @@ type PublishEvalReportResult = {
224
261
  /** Reads, merges, and publishes eval reports to GitHub Actions surfaces. */
225
262
  declare function publishEvalReport(options: PublishEvalReportOptions): Promise<PublishEvalReportResult>;
226
263
 
227
- export { type AnnotationOptions, type CheckAnnotation, type EvalCase, type EvalFailure, type EvalGatePolicy, type EvalGateResult, type EvalReport, type PublishCheckRunOptions, type PublishCheckRunResult, type PublishEvalReportOptions, type PublishEvalReportResult, type SummaryOptions, buildCheckAnnotations, collectEvalReport, computePassRate, evaluateEvalGate, formatPercent, publishCheckRun, publishEvalReport, renderGateWorkflowCommand, renderJobSummary, renderWorkflowCommands };
264
+ export { type AnnotationOptions, type CheckAnnotation, type EvalCase, type EvalFailure, type EvalGatePolicy, type EvalGateResult, type EvalReport, type PublishCheckRunOptions, type PublishCheckRunResult, type PublishEvalReportOptions, type PublishEvalReportResult, type SummaryOptions, buildCheckAnnotations, collectEvalReport, computePassRate, evaluateEvalGate, formatPercent, publishCheckRun, publishEvalReport, renderGateWorkflowCommand, renderJobSummary, renderWorkflowCommands, resolveCheckDetailsUrl, resolveCheckSha };
package/dist/index.js CHANGED
@@ -29,7 +29,9 @@ __export(index_exports, {
29
29
  publishEvalReport: () => publishEvalReport,
30
30
  renderGateWorkflowCommand: () => renderGateWorkflowCommand,
31
31
  renderJobSummary: () => renderJobSummary,
32
- renderWorkflowCommands: () => renderWorkflowCommands
32
+ renderWorkflowCommands: () => renderWorkflowCommands,
33
+ resolveCheckDetailsUrl: () => resolveCheckDetailsUrl,
34
+ resolveCheckSha: () => resolveCheckSha
33
35
  });
34
36
  module.exports = __toCommonJS(index_exports);
35
37
 
@@ -126,7 +128,8 @@ function collectEvalReport(input, options = {}) {
126
128
  const cases = workspace.cases.map(collectEvalCase);
127
129
  const failures = cases.filter((testCase) => testCase.status === "failed");
128
130
  const evalScores = cases.map((testCase) => testCase.eval?.avgScore).filter((score) => isFiniteNumber(score));
129
- const usage = sumUsage(cases);
131
+ const usage = sumAppUsage(cases);
132
+ const judgeUsage = sumJudgeUsage(cases);
130
133
  const durationMs = workspace.runs[0]?.durationMs;
131
134
  return {
132
135
  status: input.success && failures.length === 0 ? "passed" : "failed",
@@ -146,6 +149,7 @@ function collectEvalReport(input, options = {}) {
146
149
  minimum: Math.min(...evalScores)
147
150
  } : void 0,
148
151
  usage,
152
+ judgeUsage,
149
153
  cases,
150
154
  failures
151
155
  };
@@ -270,24 +274,67 @@ function stringifyReason(value) {
270
274
  }
271
275
  return typeof value === "string" ? value : stringifyValue(value, 4e3);
272
276
  }
273
- function sumUsage(cases) {
274
- const usage = {
277
+ function emptyUsage() {
278
+ return {
275
279
  inputTokens: 0,
276
280
  outputTokens: 0,
277
281
  reasoningTokens: 0,
278
282
  totalTokens: 0,
279
283
  toolCalls: 0
280
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 = [];
281
319
  for (const testCase of cases) {
282
- const caseUsage = testCase.harness?.usage;
283
- usage.inputTokens += caseUsage?.inputTokens ?? 0;
284
- usage.outputTokens += caseUsage?.outputTokens ?? 0;
285
- usage.reasoningTokens += caseUsage?.reasoningTokens ?? 0;
286
- usage.totalTokens += caseUsage?.totalTokens ?? (caseUsage?.inputTokens ?? 0) + (caseUsage?.outputTokens ?? 0) + (caseUsage?.reasoningTokens ?? 0);
287
- 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
+ }
288
326
  }
327
+ omitPartialCost(usage, runUsages);
289
328
  return usage;
290
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
+ }
291
338
  function toolCallCount(testCase) {
292
339
  const usageToolCalls = testCase.harness?.usage?.toolCalls;
293
340
  if (usageToolCalls !== void 0) {
@@ -431,8 +478,8 @@ function evaluateEvalGate(report, policy = {}) {
431
478
  status: "failed",
432
479
  enforced: true,
433
480
  passRate,
434
- title: "Eval report hard failure",
435
- 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}`
436
483
  };
437
484
  }
438
485
  if (report.totals.evalTotal === 0) {
@@ -441,7 +488,7 @@ function evaluateEvalGate(report, policy = {}) {
441
488
  status: "failed",
442
489
  enforced: true,
443
490
  passRate: null,
444
- title: "Eval report hard failure",
491
+ title: "Eval run failed",
445
492
  message: "no eval cases were reported"
446
493
  };
447
494
  }
@@ -451,8 +498,8 @@ function evaluateEvalGate(report, policy = {}) {
451
498
  status: "failed",
452
499
  enforced: true,
453
500
  passRate,
454
- title: "Eval report hard failure",
455
- message: `vitest run failed without counted test failures; ${counts}`
501
+ title: "Eval run failed",
502
+ message: `Vitest failed without reporting a failed test; ${counts}`
456
503
  };
457
504
  }
458
505
  if (minPassRate !== void 0 && (passRate === null || passRate + Number.EPSILON < minPassRate)) {
@@ -462,7 +509,7 @@ function evaluateEvalGate(report, policy = {}) {
462
509
  enforced: true,
463
510
  passRate,
464
511
  title: `Eval pass rate ${formatPercent(passRate)} \u2014 required ${formatPercent(minPassRate)}`,
465
- message: `eval pass rate below floor: ${counts}; required >= ${formatPercent(minPassRate)}`
512
+ message: `pass rate is below the minimum: ${counts}; minimum ${formatPercent(minPassRate)}`
466
513
  };
467
514
  }
468
515
  if (minScoreAverage !== void 0) {
@@ -473,8 +520,8 @@ function evaluateEvalGate(report, policy = {}) {
473
520
  status: "failed",
474
521
  enforced: true,
475
522
  passRate,
476
- title: "Eval score gate failed",
477
- 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)}`
478
525
  };
479
526
  }
480
527
  if (average + Number.EPSILON < minScoreAverage) {
@@ -483,8 +530,8 @@ function evaluateEvalGate(report, policy = {}) {
483
530
  status: "failed",
484
531
  enforced: true,
485
532
  passRate,
486
- title: `Avg score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
487
- 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)}`
488
535
  };
489
536
  }
490
537
  }
@@ -494,7 +541,7 @@ function evaluateEvalGate(report, policy = {}) {
494
541
  enforced: true,
495
542
  passRate,
496
543
  title: enforcedPassTitle(report, passRate, minPassRate, minScoreAverage),
497
- message: `eval gate passed: ${counts}${formatFloorSuffix(minPassRate, minScoreAverage)}`
544
+ message: `requirements met: ${counts}${formatMinimumSuffix(minPassRate, minScoreAverage)}`
498
545
  };
499
546
  }
500
547
  function computePassRate(report) {
@@ -527,7 +574,7 @@ function resolveMinPassRate(policy) {
527
574
  }
528
575
  function defaultCheckTitle(report) {
529
576
  if (report.failures.length === 0 && report.status === "passed") {
530
- return "No eval failures";
577
+ return "No eval cases failed";
531
578
  }
532
579
  if (report.failures.length === 0) {
533
580
  return "Vitest run failed";
@@ -536,10 +583,10 @@ function defaultCheckTitle(report) {
536
583
  }
537
584
  function enforcedPassTitle(report, passRate, minPassRate, minScoreAverage) {
538
585
  if (minPassRate !== void 0) {
539
- return `Eval pass rate ${formatPercent(passRate)} \u2014 floor ${formatPercent(minPassRate)}`;
586
+ return `Eval pass rate ${formatPercent(passRate)} \u2014 minimum ${formatPercent(minPassRate)}`;
540
587
  }
541
588
  if (minScoreAverage !== void 0) {
542
- return `Avg score ${formatScore(report.score?.average)} \u2014 floor ${formatScore(minScoreAverage)}`;
589
+ return `Average score ${formatScore(report.score?.average)} \u2014 minimum ${formatScore(minScoreAverage)}`;
543
590
  }
544
591
  return defaultCheckTitle(report);
545
592
  }
@@ -548,19 +595,22 @@ function formatEvalCounts(report, passRate) {
548
595
  const passRateText = passRate === null ? "n/a" : formatPercent(passRate);
549
596
  return `${formatNumber(report.totals.evalPassed)}/${formatNumber(
550
597
  report.totals.evalTotal
551
- )} passed (${passRateText}), avg score ${scoreText}`;
598
+ )} passed (${passRateText}), average score ${scoreText}`;
552
599
  }
553
- function formatFloorSuffix(minPassRate, minScoreAverage) {
600
+ function formatMinimumSuffix(minPassRate, minScoreAverage) {
554
601
  const parts = [];
555
602
  if (minPassRate !== void 0) {
556
- parts.push(`pass rate floor ${formatPercent(minPassRate)}`);
603
+ parts.push(`minimum pass rate ${formatPercent(minPassRate)}`);
557
604
  }
558
605
  if (minScoreAverage !== void 0) {
559
- parts.push(`avg score floor ${formatScore(minScoreAverage)}`);
606
+ parts.push(`minimum average score ${formatScore(minScoreAverage)}`);
560
607
  }
561
608
  return parts.length === 0 ? "" : `; ${parts.join(", ")}`;
562
609
  }
563
610
 
611
+ // src/github.ts
612
+ var import_node_fs = require("fs");
613
+
564
614
  // src/summary.ts
565
615
  var DEFAULT_MAX_FAILURES = 20;
566
616
  var DEFAULT_MAX_REASON_CHARS = 8e3;
@@ -588,23 +638,23 @@ function renderJobSummary(report, options = {}) {
588
638
  ""
589
639
  ];
590
640
  if (report.failures.length > 0) {
591
- const failureHeading = options.gate?.ok === true ? "### Quality Misses" : "### Failures";
641
+ const failureHeading = options.gate?.ok === true ? "### Cases Below Target" : "### Failures";
592
642
  lines.push(failureHeading, "");
593
643
  failures.forEach((testCase, index) => {
594
644
  lines.push(...renderFailureDetails(testCase, index + 1, options), "");
595
645
  });
596
646
  if (report.failures.length > failures.length) {
597
- const omittedLabel = options.gate?.ok === true ? "quality misses" : "failures";
647
+ const omittedLabel = options.gate?.ok === true ? "cases below target" : "failures";
598
648
  lines.push(
599
649
  `${report.failures.length - failures.length} more ${omittedLabel} omitted from this summary.`,
600
650
  ""
601
651
  );
602
652
  }
603
653
  } else if (report.totals.evalTotal > 0) {
604
- lines.push("### Failures", "", "No eval failures.", "");
654
+ lines.push("### Failures", "", "No eval cases failed.", "");
605
655
  }
606
656
  if (report.totals.evalTotal === 0) {
607
- 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.", "");
608
658
  }
609
659
  return `${lines.join("\n")}
610
660
  `;
@@ -614,9 +664,9 @@ function formatCountLine(passed, failed, total) {
614
664
  }
615
665
  function renderSummaryTable(report, nonEvalFailures, gate) {
616
666
  const rows = [
617
- ["Status", gate?.status ?? report.status],
667
+ ["Status", capitalize(gate?.status ?? report.status)],
618
668
  [
619
- "Evals",
669
+ "Eval cases",
620
670
  formatCountLine(
621
671
  report.totals.evalPassed,
622
672
  report.totals.evalFailed,
@@ -625,23 +675,31 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
625
675
  ]
626
676
  ];
627
677
  if (gate?.passRate !== void 0 && gate.passRate !== null) {
628
- rows.push(["Pass Rate", formatPercent(gate.passRate)]);
678
+ rows.push(["Pass rate", formatPercent(gate.passRate)]);
629
679
  } else if (report.totals.evalTotal > 0) {
630
680
  rows.push([
631
- "Pass Rate",
681
+ "Pass rate",
632
682
  formatPercent(report.totals.evalPassed / report.totals.evalTotal)
633
683
  ]);
634
684
  }
635
685
  if (report.score) {
636
686
  rows.push(["Score", formatScoreSummary(report.score)]);
637
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
+ }
638
696
  if (gate?.enforced) {
639
- rows.push(["Gate", gate.message]);
697
+ rows.push(["Requirements", gate.message]);
640
698
  }
641
699
  if (nonEvalFailures > 0) {
642
700
  rows.push([
643
- "Other Failures",
644
- `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}`
701
+ "Other test failures",
702
+ `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases`
645
703
  ]);
646
704
  }
647
705
  rows.push(["Duration", formatDuration(report.durationMs)]);
@@ -654,7 +712,53 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
654
712
  ];
655
713
  }
656
714
  function formatScoreSummary(score) {
657
- 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";
658
762
  }
659
763
  function escapeTableCell(value) {
660
764
  return value.replace(/\r?\n/g, " ").replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
@@ -734,7 +838,7 @@ function renderFailureBlock(testCase, {
734
838
  ["Case", `${number}. ${testCase.displayName}`],
735
839
  ["Status", testCase.status],
736
840
  ["Location", formatLocation(testCase.displayFile, testCase.location)],
737
- ["Harness", testCase.harness?.name ?? "n/a"],
841
+ ["App runner", testCase.harness?.name ?? "n/a"],
738
842
  ["Score", formatScore(failure?.score ?? testCase.eval?.avgScore)],
739
843
  ["Judge", failure?.judgeName ?? "n/a"]
740
844
  ];
@@ -772,7 +876,7 @@ function renderFailureBlock(testCase, {
772
876
  if (finalOutput !== void 0) {
773
877
  lines.push(
774
878
  ...renderAsciiSection(
775
- "Final Output",
879
+ "Output",
776
880
  stringifyValue(finalOutput, maxOutputChars).split(/\r?\n/)
777
881
  ),
778
882
  ""
@@ -800,7 +904,7 @@ function renderFailureBlock(testCase, {
800
904
  if (testCase.harness?.errors.length) {
801
905
  lines.push(
802
906
  ...renderAsciiSection(
803
- "Harness Errors",
907
+ "App errors",
804
908
  stringifyValue(testCase.harness.errors, maxReasonChars).split(/\r?\n/)
805
909
  ),
806
910
  ""
@@ -849,7 +953,9 @@ function formatCaseUsage(testCase) {
849
953
  parts.push(`${formatNumber(totalTokens)} tokens`);
850
954
  }
851
955
  if (toolCalls > 0) {
852
- parts.push(`${formatNumber(toolCalls)} tool${toolCalls === 1 ? "" : "s"}`);
956
+ parts.push(
957
+ `${formatNumber(toolCalls)} tool call${toolCalls === 1 ? "" : "s"}`
958
+ );
853
959
  }
854
960
  if (testCase.harness?.timingMs !== void 0) {
855
961
  parts.push(formatDuration(testCase.harness.timingMs));
@@ -861,10 +967,44 @@ function formatCaseUsage(testCase) {
861
967
  var DEFAULT_CHECK_NAME = "vitest-evals";
862
968
  var MAX_CHECK_SUMMARY_LENGTH = 64e3;
863
969
  var CHECK_SUMMARY_TRUNCATION_SUFFIX = "\n\n[truncated for GitHub Check Run]\n";
970
+ function resolveCheckSha(env = process.env, options = {}) {
971
+ const explicit = options.sha?.trim() || env.GITHUB_PR_HEAD_SHA?.trim();
972
+ if (explicit) {
973
+ return explicit;
974
+ }
975
+ const eventPath = options.eventPath?.trim() || env.GITHUB_EVENT_PATH?.trim();
976
+ if (eventPath) {
977
+ try {
978
+ const event = JSON.parse((0, import_node_fs.readFileSync)(eventPath, "utf8"));
979
+ const headSha = event.pull_request?.head?.sha;
980
+ if (typeof headSha === "string" && headSha.trim()) {
981
+ return headSha.trim();
982
+ }
983
+ } catch {
984
+ }
985
+ }
986
+ return env.GITHUB_SHA?.trim() || void 0;
987
+ }
988
+ function resolveCheckDetailsUrl(env = process.env, options = {}) {
989
+ const explicit = options.detailsUrl?.trim();
990
+ if (explicit) {
991
+ return explicit;
992
+ }
993
+ const server = env.GITHUB_SERVER_URL?.replace(/\/$/, "");
994
+ const repository = env.GITHUB_REPOSITORY?.trim();
995
+ const runId = env.GITHUB_RUN_ID?.trim();
996
+ if (!server || !repository || !runId) {
997
+ return void 0;
998
+ }
999
+ return `${server}/${repository}/actions/runs/${runId}`;
1000
+ }
864
1001
  async function publishCheckRun(report, options = {}) {
865
1002
  const token = options.token ?? process.env.GITHUB_TOKEN;
866
1003
  const repository = options.repository ?? process.env.GITHUB_REPOSITORY;
867
- const sha = options.sha ?? process.env.GITHUB_SHA;
1004
+ const sha = resolveCheckSha(process.env, { sha: options.sha });
1005
+ const detailsUrl = resolveCheckDetailsUrl(process.env, {
1006
+ detailsUrl: options.detailsUrl
1007
+ });
868
1008
  if (!token) {
869
1009
  return { status: "skipped", reason: "missing GITHUB_TOKEN" };
870
1010
  }
@@ -872,7 +1012,10 @@ async function publishCheckRun(report, options = {}) {
872
1012
  return { status: "skipped", reason: "missing GITHUB_REPOSITORY" };
873
1013
  }
874
1014
  if (!sha && options.checkRunId === void 0) {
875
- return { status: "skipped", reason: "missing GITHUB_SHA" };
1015
+ return {
1016
+ status: "skipped",
1017
+ reason: "missing commit SHA (set --sha / options.sha, GITHUB_PR_HEAD_SHA, pull_request.head.sha, or GITHUB_SHA)"
1018
+ };
876
1019
  }
877
1020
  const [owner, repo] = repository.split("/");
878
1021
  if (!owner || !repo) {
@@ -881,7 +1024,7 @@ async function publishCheckRun(report, options = {}) {
881
1024
  reason: `invalid GitHub repository: ${repository}`
882
1025
  };
883
1026
  }
884
- const payload = buildCheckRunPayload(report, options);
1027
+ const payload = buildCheckRunPayload(report, options, detailsUrl);
885
1028
  const apiUrl = options.apiUrl ?? process.env.GITHUB_API_URL ?? "https://api.github.com";
886
1029
  const requestUrl = options.checkRunId === void 0 ? `${apiUrl}/repos/${owner}/${repo}/check-runs` : `${apiUrl}/repos/${owner}/${repo}/check-runs/${options.checkRunId}`;
887
1030
  const response = await fetch(requestUrl, {
@@ -896,6 +1039,7 @@ async function publishCheckRun(report, options = {}) {
896
1039
  options.checkRunId === void 0 ? {
897
1040
  name: options.name ?? DEFAULT_CHECK_NAME,
898
1041
  head_sha: sha,
1042
+ ...options.externalId ? { external_id: options.externalId } : {},
899
1043
  ...payload
900
1044
  } : payload
901
1045
  )
@@ -910,10 +1054,11 @@ async function publishCheckRun(report, options = {}) {
910
1054
  return {
911
1055
  status: options.checkRunId === void 0 ? "created" : "updated",
912
1056
  id: data.id,
913
- htmlUrl: data.html_url
1057
+ htmlUrl: data.html_url,
1058
+ sha
914
1059
  };
915
1060
  }
916
- function buildCheckRunPayload(report, options) {
1061
+ function buildCheckRunPayload(report, options, detailsUrl) {
917
1062
  const gate = options.gate ?? evaluateEvalGate(report);
918
1063
  const annotations = buildCheckAnnotations(report, {
919
1064
  maxAnnotations: options.maxAnnotations,
@@ -923,6 +1068,7 @@ function buildCheckRunPayload(report, options) {
923
1068
  status: "completed",
924
1069
  conclusion: gate.ok ? "success" : "failure",
925
1070
  completed_at: (/* @__PURE__ */ new Date()).toISOString(),
1071
+ ...detailsUrl ? { details_url: detailsUrl } : {},
926
1072
  output: {
927
1073
  title: gate.title,
928
1074
  summary: truncateCheckSummary(
@@ -979,16 +1125,22 @@ function mergeEvalReports(reports) {
979
1125
  minimum: Math.min(...scoredCases)
980
1126
  } : void 0,
981
1127
  usage: mergeUsage(reports.map((report) => report.usage)),
1128
+ judgeUsage: mergeUsage(reports.map((report) => report.judgeUsage)),
982
1129
  cases,
983
1130
  failures
984
1131
  };
985
1132
  }
986
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
+ );
987
1138
  return {
988
1139
  inputTokens: sum(usages, (usage) => usage.inputTokens),
989
1140
  outputTokens: sum(usages, (usage) => usage.outputTokens),
990
1141
  reasoningTokens: sum(usages, (usage) => usage.reasoningTokens),
991
1142
  totalTokens: sum(usages, (usage) => usage.totalTokens),
1143
+ ...costComplete && costs.length > 0 ? { costUsd: costs.reduce((total, cost) => total + cost, 0) } : {},
992
1144
  toolCalls: sum(usages, (usage) => usage.toolCalls)
993
1145
  };
994
1146
  }
@@ -1080,11 +1232,17 @@ async function publishEvalReport(options) {
1080
1232
  name: options.checkName,
1081
1233
  repository: options.repository,
1082
1234
  sha: options.sha,
1235
+ detailsUrl: options.detailsUrl,
1236
+ externalId: options.externalId,
1083
1237
  token: options.token,
1084
1238
  gate
1085
1239
  });
1086
1240
  if (checkRun.status === "skipped") {
1087
1241
  options.warn?.(`GitHub Check Run skipped: ${checkRun.reason}`);
1242
+ } else if (checkRun.htmlUrl) {
1243
+ console.log(`published check run: ${checkRun.htmlUrl}`);
1244
+ } else if (checkRun.id !== void 0) {
1245
+ console.log(`published check run id: ${checkRun.id}`);
1088
1246
  }
1089
1247
  } catch (error) {
1090
1248
  const message = error instanceof Error ? error.message : String(error);
@@ -1094,14 +1252,21 @@ async function publishEvalReport(options) {
1094
1252
  options.warn?.(message);
1095
1253
  }
1096
1254
  }
1255
+ const gateFailed = gate.enforced && !gate.ok;
1256
+ const wantsSoftFail = options.softFail ?? options.checkRun === true;
1257
+ const softFail = wantsSoftFail && checkRunPublished(checkRun);
1258
+ const shouldFail = gateFailed && !softFail;
1097
1259
  return {
1098
1260
  report,
1099
1261
  resultFiles,
1100
1262
  gate,
1101
- shouldFail: gate.enforced && !gate.ok,
1263
+ shouldFail,
1102
1264
  checkRun
1103
1265
  };
1104
1266
  }
1267
+ function checkRunPublished(checkRun) {
1268
+ return checkRun?.status === "created" || checkRun?.status === "updated";
1269
+ }
1105
1270
  // Annotate the CommonJS export names for ESM import in node:
1106
1271
  0 && (module.exports = {
1107
1272
  buildCheckAnnotations,
@@ -1113,6 +1278,8 @@ async function publishEvalReport(options) {
1113
1278
  publishEvalReport,
1114
1279
  renderGateWorkflowCommand,
1115
1280
  renderJobSummary,
1116
- renderWorkflowCommands
1281
+ renderWorkflowCommands,
1282
+ resolveCheckDetailsUrl,
1283
+ resolveCheckSha
1117
1284
  });
1118
1285
  //# sourceMappingURL=index.js.map