@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/README.md +16 -3
- package/dist/cli.js +222 -50
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +222 -50
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +41 -4
- package/dist/index.d.ts +41 -4
- package/dist/index.js +218 -51
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +215 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -31,10 +31,19 @@ steps:
|
|
|
31
31
|
with:
|
|
32
32
|
results: vitest-results.json
|
|
33
33
|
publish-check: true
|
|
34
|
+
min-pass-rate: 0.8
|
|
34
35
|
```
|
|
35
36
|
|
|
37
|
+
On `pull_request`, the Check Run attaches to the PR head SHA (not the temporary
|
|
38
|
+
merge `GITHUB_SHA`) so it shows on the PR checks list. Override with the `sha`
|
|
39
|
+
input when needed.
|
|
40
|
+
|
|
41
|
+
When a Check Run publishes successfully with a gate, the action step soft-fails
|
|
42
|
+
by default so the Check Run owns green/red. If publishing is skipped, the step
|
|
43
|
+
still fails on a rejected gate. Set `soft-fail: false` to also fail the job.
|
|
44
|
+
|
|
36
45
|
If configuration or permission is missing, the action keeps the job summary and
|
|
37
|
-
workflow annotations and warns instead of failing.
|
|
46
|
+
workflow annotations and warns instead of failing solely for the missing check.
|
|
38
47
|
|
|
39
48
|
## Score and Pass-Rate Gates
|
|
40
49
|
|
|
@@ -49,9 +58,11 @@ workflow annotations and warns instead of failing.
|
|
|
49
58
|
|
|
50
59
|
- `fail-on-failures: true` requires every eval case to pass
|
|
51
60
|
- `min-pass-rate` and `min-score-average` set aggregate floors in the `0`-`1` range
|
|
52
|
-
- `status
|
|
61
|
+
- `status` and Check Run conclusion/title follow the gate
|
|
53
62
|
- quality misses become warnings when the gate still passes
|
|
54
63
|
- non-eval / infrastructure failures still fail hard
|
|
64
|
+
- published Check Runs soft-fail the step by default; set `soft-fail: false` to
|
|
65
|
+
also fail the workflow job
|
|
55
66
|
- use `evals-failed` / `pass-rate` for raw tallies (`pass-rate` is a 0-1 ratio)
|
|
56
67
|
|
|
57
68
|
## Sharded Reports
|
|
@@ -80,10 +91,12 @@ final reducer job:
|
|
|
80
91
|
| `results` | `vitest-results.json` | Vitest JSON result files. Supports paths, `*` and `**` globs, and newline-separated entries. |
|
|
81
92
|
| `publish-summary` | `true` | Write a GitHub Actions job summary. |
|
|
82
93
|
| `publish-annotations` | `true` | Emit GitHub workflow annotations for failed evals. |
|
|
83
|
-
| `publish-check` | `false` | Publish one GitHub Check Run for the combined report. |
|
|
94
|
+
| `publish-check` | `false` | Publish one GitHub Check Run for the combined report. Attaches to PR head on `pull_request`. |
|
|
84
95
|
| `check-name` | `vitest-evals` | Name of the GitHub Check Run. |
|
|
85
96
|
| `github-token` | `${{ github.token }}` | Token used for Check Run publishing. |
|
|
97
|
+
| `sha` | PR head, else `GITHUB_SHA` | Commit SHA for the Check Run. |
|
|
86
98
|
| `fail-on-failures` | `false` | Fail the action when any eval case failed. Equivalent to `min-pass-rate: 1`. |
|
|
99
|
+
| `soft-fail` | auto | Keep the step green when a published Check Run owns a failed gate. |
|
|
87
100
|
| `min-pass-rate` | unset | Minimum fraction of eval cases that must pass (`0`-`1`). |
|
|
88
101
|
| `min-score-average` | unset | Minimum average eval score across scored cases (`0`-`1`). |
|
|
89
102
|
| `max-annotations` | unset | Maximum number of failure annotations to publish. Check Run annotations are capped at 50 by GitHub. |
|
package/dist/cli.js
CHANGED
|
@@ -41,6 +41,12 @@ function parseCliArgs(args, env = process.env) {
|
|
|
41
41
|
case "--fail-on-check-error":
|
|
42
42
|
options.failOnCheckError = true;
|
|
43
43
|
break;
|
|
44
|
+
case "--soft-fail":
|
|
45
|
+
options.softFail = true;
|
|
46
|
+
break;
|
|
47
|
+
case "--no-soft-fail":
|
|
48
|
+
options.softFail = false;
|
|
49
|
+
break;
|
|
44
50
|
case "--min-pass-rate":
|
|
45
51
|
options.minPassRate = readRatio(args, ++index, arg);
|
|
46
52
|
break;
|
|
@@ -317,7 +323,8 @@ function collectEvalReport(input, options = {}) {
|
|
|
317
323
|
const cases = workspace.cases.map(collectEvalCase);
|
|
318
324
|
const failures = cases.filter((testCase) => testCase.status === "failed");
|
|
319
325
|
const evalScores = cases.map((testCase) => testCase.eval?.avgScore).filter((score) => isFiniteNumber(score));
|
|
320
|
-
const usage2 =
|
|
326
|
+
const usage2 = sumAppUsage(cases);
|
|
327
|
+
const judgeUsage = sumJudgeUsage(cases);
|
|
321
328
|
const durationMs = workspace.runs[0]?.durationMs;
|
|
322
329
|
return {
|
|
323
330
|
status: input.success && failures.length === 0 ? "passed" : "failed",
|
|
@@ -337,6 +344,7 @@ function collectEvalReport(input, options = {}) {
|
|
|
337
344
|
minimum: Math.min(...evalScores)
|
|
338
345
|
} : void 0,
|
|
339
346
|
usage: usage2,
|
|
347
|
+
judgeUsage,
|
|
340
348
|
cases,
|
|
341
349
|
failures
|
|
342
350
|
};
|
|
@@ -461,24 +469,67 @@ function stringifyReason(value) {
|
|
|
461
469
|
}
|
|
462
470
|
return typeof value === "string" ? value : stringifyValue(value, 4e3);
|
|
463
471
|
}
|
|
464
|
-
function
|
|
465
|
-
|
|
472
|
+
function emptyUsage() {
|
|
473
|
+
return {
|
|
466
474
|
inputTokens: 0,
|
|
467
475
|
outputTokens: 0,
|
|
468
476
|
reasoningTokens: 0,
|
|
469
477
|
totalTokens: 0,
|
|
470
478
|
toolCalls: 0
|
|
471
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 = [];
|
|
472
514
|
for (const testCase of cases) {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
+
}
|
|
479
521
|
}
|
|
522
|
+
omitPartialCost(usage2, runUsages);
|
|
480
523
|
return usage2;
|
|
481
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
|
+
}
|
|
482
533
|
function toolCallCount(testCase) {
|
|
483
534
|
const usageToolCalls = testCase.harness?.usage?.toolCalls;
|
|
484
535
|
if (usageToolCalls !== void 0) {
|
|
@@ -515,8 +566,8 @@ function evaluateEvalGate(report, policy = {}) {
|
|
|
515
566
|
status: "failed",
|
|
516
567
|
enforced: true,
|
|
517
568
|
passRate,
|
|
518
|
-
title: "Eval
|
|
519
|
-
message: `${formatNumber(nonEvalFailures)}
|
|
569
|
+
title: "Eval run failed",
|
|
570
|
+
message: `${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases; ${counts}`
|
|
520
571
|
};
|
|
521
572
|
}
|
|
522
573
|
if (report.totals.evalTotal === 0) {
|
|
@@ -525,7 +576,7 @@ function evaluateEvalGate(report, policy = {}) {
|
|
|
525
576
|
status: "failed",
|
|
526
577
|
enforced: true,
|
|
527
578
|
passRate: null,
|
|
528
|
-
title: "Eval
|
|
579
|
+
title: "Eval run failed",
|
|
529
580
|
message: "no eval cases were reported"
|
|
530
581
|
};
|
|
531
582
|
}
|
|
@@ -535,8 +586,8 @@ function evaluateEvalGate(report, policy = {}) {
|
|
|
535
586
|
status: "failed",
|
|
536
587
|
enforced: true,
|
|
537
588
|
passRate,
|
|
538
|
-
title: "Eval
|
|
539
|
-
message: `
|
|
589
|
+
title: "Eval run failed",
|
|
590
|
+
message: `Vitest failed without reporting a failed test; ${counts}`
|
|
540
591
|
};
|
|
541
592
|
}
|
|
542
593
|
if (minPassRate !== void 0 && (passRate === null || passRate + Number.EPSILON < minPassRate)) {
|
|
@@ -546,7 +597,7 @@ function evaluateEvalGate(report, policy = {}) {
|
|
|
546
597
|
enforced: true,
|
|
547
598
|
passRate,
|
|
548
599
|
title: `Eval pass rate ${formatPercent(passRate)} \u2014 required ${formatPercent(minPassRate)}`,
|
|
549
|
-
message: `
|
|
600
|
+
message: `pass rate is below the minimum: ${counts}; minimum ${formatPercent(minPassRate)}`
|
|
550
601
|
};
|
|
551
602
|
}
|
|
552
603
|
if (minScoreAverage !== void 0) {
|
|
@@ -557,8 +608,8 @@ function evaluateEvalGate(report, policy = {}) {
|
|
|
557
608
|
status: "failed",
|
|
558
609
|
enforced: true,
|
|
559
610
|
passRate,
|
|
560
|
-
title: "
|
|
561
|
-
message: `no score
|
|
611
|
+
title: "Average score unavailable",
|
|
612
|
+
message: `no average score was reported; minimum ${formatScore(minScoreAverage)}`
|
|
562
613
|
};
|
|
563
614
|
}
|
|
564
615
|
if (average + Number.EPSILON < minScoreAverage) {
|
|
@@ -567,8 +618,8 @@ function evaluateEvalGate(report, policy = {}) {
|
|
|
567
618
|
status: "failed",
|
|
568
619
|
enforced: true,
|
|
569
620
|
passRate,
|
|
570
|
-
title: `
|
|
571
|
-
message: `
|
|
621
|
+
title: `Average score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
|
|
622
|
+
message: `average score is below the minimum: ${counts}; minimum ${formatScore(minScoreAverage)}`
|
|
572
623
|
};
|
|
573
624
|
}
|
|
574
625
|
}
|
|
@@ -578,7 +629,7 @@ function evaluateEvalGate(report, policy = {}) {
|
|
|
578
629
|
enforced: true,
|
|
579
630
|
passRate,
|
|
580
631
|
title: enforcedPassTitle(report, passRate, minPassRate, minScoreAverage),
|
|
581
|
-
message: `
|
|
632
|
+
message: `requirements met: ${counts}${formatMinimumSuffix(minPassRate, minScoreAverage)}`
|
|
582
633
|
};
|
|
583
634
|
}
|
|
584
635
|
function computePassRate(report) {
|
|
@@ -611,7 +662,7 @@ function resolveMinPassRate(policy) {
|
|
|
611
662
|
}
|
|
612
663
|
function defaultCheckTitle(report) {
|
|
613
664
|
if (report.failures.length === 0 && report.status === "passed") {
|
|
614
|
-
return "No eval
|
|
665
|
+
return "No eval cases failed";
|
|
615
666
|
}
|
|
616
667
|
if (report.failures.length === 0) {
|
|
617
668
|
return "Vitest run failed";
|
|
@@ -620,10 +671,10 @@ function defaultCheckTitle(report) {
|
|
|
620
671
|
}
|
|
621
672
|
function enforcedPassTitle(report, passRate, minPassRate, minScoreAverage) {
|
|
622
673
|
if (minPassRate !== void 0) {
|
|
623
|
-
return `Eval pass rate ${formatPercent(passRate)} \u2014
|
|
674
|
+
return `Eval pass rate ${formatPercent(passRate)} \u2014 minimum ${formatPercent(minPassRate)}`;
|
|
624
675
|
}
|
|
625
676
|
if (minScoreAverage !== void 0) {
|
|
626
|
-
return `
|
|
677
|
+
return `Average score ${formatScore(report.score?.average)} \u2014 minimum ${formatScore(minScoreAverage)}`;
|
|
627
678
|
}
|
|
628
679
|
return defaultCheckTitle(report);
|
|
629
680
|
}
|
|
@@ -632,19 +683,22 @@ function formatEvalCounts(report, passRate) {
|
|
|
632
683
|
const passRateText = passRate === null ? "n/a" : formatPercent(passRate);
|
|
633
684
|
return `${formatNumber(report.totals.evalPassed)}/${formatNumber(
|
|
634
685
|
report.totals.evalTotal
|
|
635
|
-
)} passed (${passRateText}),
|
|
686
|
+
)} passed (${passRateText}), average score ${scoreText}`;
|
|
636
687
|
}
|
|
637
|
-
function
|
|
688
|
+
function formatMinimumSuffix(minPassRate, minScoreAverage) {
|
|
638
689
|
const parts = [];
|
|
639
690
|
if (minPassRate !== void 0) {
|
|
640
|
-
parts.push(`pass rate
|
|
691
|
+
parts.push(`minimum pass rate ${formatPercent(minPassRate)}`);
|
|
641
692
|
}
|
|
642
693
|
if (minScoreAverage !== void 0) {
|
|
643
|
-
parts.push(`
|
|
694
|
+
parts.push(`minimum average score ${formatScore(minScoreAverage)}`);
|
|
644
695
|
}
|
|
645
696
|
return parts.length === 0 ? "" : `; ${parts.join(", ")}`;
|
|
646
697
|
}
|
|
647
698
|
|
|
699
|
+
// src/github.ts
|
|
700
|
+
var import_node_fs = require("fs");
|
|
701
|
+
|
|
648
702
|
// src/summary.ts
|
|
649
703
|
var DEFAULT_MAX_FAILURES = 20;
|
|
650
704
|
var DEFAULT_MAX_REASON_CHARS = 8e3;
|
|
@@ -672,23 +726,23 @@ function renderJobSummary(report, options = {}) {
|
|
|
672
726
|
""
|
|
673
727
|
];
|
|
674
728
|
if (report.failures.length > 0) {
|
|
675
|
-
const failureHeading = options.gate?.ok === true ? "###
|
|
729
|
+
const failureHeading = options.gate?.ok === true ? "### Cases Below Target" : "### Failures";
|
|
676
730
|
lines.push(failureHeading, "");
|
|
677
731
|
failures.forEach((testCase, index) => {
|
|
678
732
|
lines.push(...renderFailureDetails(testCase, index + 1, options), "");
|
|
679
733
|
});
|
|
680
734
|
if (report.failures.length > failures.length) {
|
|
681
|
-
const omittedLabel = options.gate?.ok === true ? "
|
|
735
|
+
const omittedLabel = options.gate?.ok === true ? "cases below target" : "failures";
|
|
682
736
|
lines.push(
|
|
683
737
|
`${report.failures.length - failures.length} more ${omittedLabel} omitted from this summary.`,
|
|
684
738
|
""
|
|
685
739
|
);
|
|
686
740
|
}
|
|
687
741
|
} else if (report.totals.evalTotal > 0) {
|
|
688
|
-
lines.push("### Failures", "", "No eval
|
|
742
|
+
lines.push("### Failures", "", "No eval cases failed.", "");
|
|
689
743
|
}
|
|
690
744
|
if (report.totals.evalTotal === 0) {
|
|
691
|
-
lines.push("No eval
|
|
745
|
+
lines.push("No eval results were found in the Vitest JSON report.", "");
|
|
692
746
|
}
|
|
693
747
|
return `${lines.join("\n")}
|
|
694
748
|
`;
|
|
@@ -698,9 +752,9 @@ function formatCountLine(passed, failed, total) {
|
|
|
698
752
|
}
|
|
699
753
|
function renderSummaryTable(report, nonEvalFailures, gate) {
|
|
700
754
|
const rows = [
|
|
701
|
-
["Status", gate?.status ?? report.status],
|
|
755
|
+
["Status", capitalize(gate?.status ?? report.status)],
|
|
702
756
|
[
|
|
703
|
-
"
|
|
757
|
+
"Eval cases",
|
|
704
758
|
formatCountLine(
|
|
705
759
|
report.totals.evalPassed,
|
|
706
760
|
report.totals.evalFailed,
|
|
@@ -709,23 +763,31 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
|
|
|
709
763
|
]
|
|
710
764
|
];
|
|
711
765
|
if (gate?.passRate !== void 0 && gate.passRate !== null) {
|
|
712
|
-
rows.push(["Pass
|
|
766
|
+
rows.push(["Pass rate", formatPercent(gate.passRate)]);
|
|
713
767
|
} else if (report.totals.evalTotal > 0) {
|
|
714
768
|
rows.push([
|
|
715
|
-
"Pass
|
|
769
|
+
"Pass rate",
|
|
716
770
|
formatPercent(report.totals.evalPassed / report.totals.evalTotal)
|
|
717
771
|
]);
|
|
718
772
|
}
|
|
719
773
|
if (report.score) {
|
|
720
774
|
rows.push(["Score", formatScoreSummary(report.score)]);
|
|
721
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
|
+
}
|
|
722
784
|
if (gate?.enforced) {
|
|
723
|
-
rows.push(["
|
|
785
|
+
rows.push(["Requirements", gate.message]);
|
|
724
786
|
}
|
|
725
787
|
if (nonEvalFailures > 0) {
|
|
726
788
|
rows.push([
|
|
727
|
-
"Other
|
|
728
|
-
`${formatNumber(nonEvalFailures)}
|
|
789
|
+
"Other test failures",
|
|
790
|
+
`${formatNumber(nonEvalFailures)} test failure${nonEvalFailures === 1 ? "" : "s"} outside eval cases`
|
|
729
791
|
]);
|
|
730
792
|
}
|
|
731
793
|
rows.push(["Duration", formatDuration(report.durationMs)]);
|
|
@@ -738,7 +800,53 @@ function renderSummaryTable(report, nonEvalFailures, gate) {
|
|
|
738
800
|
];
|
|
739
801
|
}
|
|
740
802
|
function formatScoreSummary(score) {
|
|
741
|
-
return `
|
|
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";
|
|
742
850
|
}
|
|
743
851
|
function escapeTableCell(value) {
|
|
744
852
|
return value.replace(/\r?\n/g, " ").replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
@@ -818,7 +926,7 @@ function renderFailureBlock(testCase, {
|
|
|
818
926
|
["Case", `${number}. ${testCase.displayName}`],
|
|
819
927
|
["Status", testCase.status],
|
|
820
928
|
["Location", formatLocation(testCase.displayFile, testCase.location)],
|
|
821
|
-
["
|
|
929
|
+
["App runner", testCase.harness?.name ?? "n/a"],
|
|
822
930
|
["Score", formatScore(failure?.score ?? testCase.eval?.avgScore)],
|
|
823
931
|
["Judge", failure?.judgeName ?? "n/a"]
|
|
824
932
|
];
|
|
@@ -856,7 +964,7 @@ function renderFailureBlock(testCase, {
|
|
|
856
964
|
if (finalOutput !== void 0) {
|
|
857
965
|
lines.push(
|
|
858
966
|
...renderAsciiSection(
|
|
859
|
-
"
|
|
967
|
+
"Output",
|
|
860
968
|
stringifyValue(finalOutput, maxOutputChars).split(/\r?\n/)
|
|
861
969
|
),
|
|
862
970
|
""
|
|
@@ -884,7 +992,7 @@ function renderFailureBlock(testCase, {
|
|
|
884
992
|
if (testCase.harness?.errors.length) {
|
|
885
993
|
lines.push(
|
|
886
994
|
...renderAsciiSection(
|
|
887
|
-
"
|
|
995
|
+
"App errors",
|
|
888
996
|
stringifyValue(testCase.harness.errors, maxReasonChars).split(/\r?\n/)
|
|
889
997
|
),
|
|
890
998
|
""
|
|
@@ -933,7 +1041,9 @@ function formatCaseUsage(testCase) {
|
|
|
933
1041
|
parts.push(`${formatNumber(totalTokens)} tokens`);
|
|
934
1042
|
}
|
|
935
1043
|
if (toolCalls > 0) {
|
|
936
|
-
parts.push(
|
|
1044
|
+
parts.push(
|
|
1045
|
+
`${formatNumber(toolCalls)} tool call${toolCalls === 1 ? "" : "s"}`
|
|
1046
|
+
);
|
|
937
1047
|
}
|
|
938
1048
|
if (testCase.harness?.timingMs !== void 0) {
|
|
939
1049
|
parts.push(formatDuration(testCase.harness.timingMs));
|
|
@@ -945,10 +1055,44 @@ function formatCaseUsage(testCase) {
|
|
|
945
1055
|
var DEFAULT_CHECK_NAME = "vitest-evals";
|
|
946
1056
|
var MAX_CHECK_SUMMARY_LENGTH = 64e3;
|
|
947
1057
|
var CHECK_SUMMARY_TRUNCATION_SUFFIX = "\n\n[truncated for GitHub Check Run]\n";
|
|
1058
|
+
function resolveCheckSha(env = process.env, options = {}) {
|
|
1059
|
+
const explicit = options.sha?.trim() || env.GITHUB_PR_HEAD_SHA?.trim();
|
|
1060
|
+
if (explicit) {
|
|
1061
|
+
return explicit;
|
|
1062
|
+
}
|
|
1063
|
+
const eventPath = options.eventPath?.trim() || env.GITHUB_EVENT_PATH?.trim();
|
|
1064
|
+
if (eventPath) {
|
|
1065
|
+
try {
|
|
1066
|
+
const event = JSON.parse((0, import_node_fs.readFileSync)(eventPath, "utf8"));
|
|
1067
|
+
const headSha = event.pull_request?.head?.sha;
|
|
1068
|
+
if (typeof headSha === "string" && headSha.trim()) {
|
|
1069
|
+
return headSha.trim();
|
|
1070
|
+
}
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return env.GITHUB_SHA?.trim() || void 0;
|
|
1075
|
+
}
|
|
1076
|
+
function resolveCheckDetailsUrl(env = process.env, options = {}) {
|
|
1077
|
+
const explicit = options.detailsUrl?.trim();
|
|
1078
|
+
if (explicit) {
|
|
1079
|
+
return explicit;
|
|
1080
|
+
}
|
|
1081
|
+
const server = env.GITHUB_SERVER_URL?.replace(/\/$/, "");
|
|
1082
|
+
const repository = env.GITHUB_REPOSITORY?.trim();
|
|
1083
|
+
const runId = env.GITHUB_RUN_ID?.trim();
|
|
1084
|
+
if (!server || !repository || !runId) {
|
|
1085
|
+
return void 0;
|
|
1086
|
+
}
|
|
1087
|
+
return `${server}/${repository}/actions/runs/${runId}`;
|
|
1088
|
+
}
|
|
948
1089
|
async function publishCheckRun(report, options = {}) {
|
|
949
1090
|
const token = options.token ?? process.env.GITHUB_TOKEN;
|
|
950
1091
|
const repository = options.repository ?? process.env.GITHUB_REPOSITORY;
|
|
951
|
-
const sha = options.sha
|
|
1092
|
+
const sha = resolveCheckSha(process.env, { sha: options.sha });
|
|
1093
|
+
const detailsUrl = resolveCheckDetailsUrl(process.env, {
|
|
1094
|
+
detailsUrl: options.detailsUrl
|
|
1095
|
+
});
|
|
952
1096
|
if (!token) {
|
|
953
1097
|
return { status: "skipped", reason: "missing GITHUB_TOKEN" };
|
|
954
1098
|
}
|
|
@@ -956,7 +1100,10 @@ async function publishCheckRun(report, options = {}) {
|
|
|
956
1100
|
return { status: "skipped", reason: "missing GITHUB_REPOSITORY" };
|
|
957
1101
|
}
|
|
958
1102
|
if (!sha && options.checkRunId === void 0) {
|
|
959
|
-
return {
|
|
1103
|
+
return {
|
|
1104
|
+
status: "skipped",
|
|
1105
|
+
reason: "missing commit SHA (set --sha / options.sha, GITHUB_PR_HEAD_SHA, pull_request.head.sha, or GITHUB_SHA)"
|
|
1106
|
+
};
|
|
960
1107
|
}
|
|
961
1108
|
const [owner, repo] = repository.split("/");
|
|
962
1109
|
if (!owner || !repo) {
|
|
@@ -965,7 +1112,7 @@ async function publishCheckRun(report, options = {}) {
|
|
|
965
1112
|
reason: `invalid GitHub repository: ${repository}`
|
|
966
1113
|
};
|
|
967
1114
|
}
|
|
968
|
-
const payload = buildCheckRunPayload(report, options);
|
|
1115
|
+
const payload = buildCheckRunPayload(report, options, detailsUrl);
|
|
969
1116
|
const apiUrl = options.apiUrl ?? process.env.GITHUB_API_URL ?? "https://api.github.com";
|
|
970
1117
|
const requestUrl = options.checkRunId === void 0 ? `${apiUrl}/repos/${owner}/${repo}/check-runs` : `${apiUrl}/repos/${owner}/${repo}/check-runs/${options.checkRunId}`;
|
|
971
1118
|
const response = await fetch(requestUrl, {
|
|
@@ -980,6 +1127,7 @@ async function publishCheckRun(report, options = {}) {
|
|
|
980
1127
|
options.checkRunId === void 0 ? {
|
|
981
1128
|
name: options.name ?? DEFAULT_CHECK_NAME,
|
|
982
1129
|
head_sha: sha,
|
|
1130
|
+
...options.externalId ? { external_id: options.externalId } : {},
|
|
983
1131
|
...payload
|
|
984
1132
|
} : payload
|
|
985
1133
|
)
|
|
@@ -994,10 +1142,11 @@ async function publishCheckRun(report, options = {}) {
|
|
|
994
1142
|
return {
|
|
995
1143
|
status: options.checkRunId === void 0 ? "created" : "updated",
|
|
996
1144
|
id: data.id,
|
|
997
|
-
htmlUrl: data.html_url
|
|
1145
|
+
htmlUrl: data.html_url,
|
|
1146
|
+
sha
|
|
998
1147
|
};
|
|
999
1148
|
}
|
|
1000
|
-
function buildCheckRunPayload(report, options) {
|
|
1149
|
+
function buildCheckRunPayload(report, options, detailsUrl) {
|
|
1001
1150
|
const gate = options.gate ?? evaluateEvalGate(report);
|
|
1002
1151
|
const annotations = buildCheckAnnotations(report, {
|
|
1003
1152
|
maxAnnotations: options.maxAnnotations,
|
|
@@ -1007,6 +1156,7 @@ function buildCheckRunPayload(report, options) {
|
|
|
1007
1156
|
status: "completed",
|
|
1008
1157
|
conclusion: gate.ok ? "success" : "failure",
|
|
1009
1158
|
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1159
|
+
...detailsUrl ? { details_url: detailsUrl } : {},
|
|
1010
1160
|
output: {
|
|
1011
1161
|
title: gate.title,
|
|
1012
1162
|
summary: truncateCheckSummary(
|
|
@@ -1059,16 +1209,22 @@ function mergeEvalReports(reports) {
|
|
|
1059
1209
|
minimum: Math.min(...scoredCases)
|
|
1060
1210
|
} : void 0,
|
|
1061
1211
|
usage: mergeUsage(reports.map((report) => report.usage)),
|
|
1212
|
+
judgeUsage: mergeUsage(reports.map((report) => report.judgeUsage)),
|
|
1062
1213
|
cases,
|
|
1063
1214
|
failures
|
|
1064
1215
|
};
|
|
1065
1216
|
}
|
|
1066
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
|
+
);
|
|
1067
1222
|
return {
|
|
1068
1223
|
inputTokens: sum(usages, (usage2) => usage2.inputTokens),
|
|
1069
1224
|
outputTokens: sum(usages, (usage2) => usage2.outputTokens),
|
|
1070
1225
|
reasoningTokens: sum(usages, (usage2) => usage2.reasoningTokens),
|
|
1071
1226
|
totalTokens: sum(usages, (usage2) => usage2.totalTokens),
|
|
1227
|
+
...costComplete && costs.length > 0 ? { costUsd: costs.reduce((total, cost) => total + cost, 0) } : {},
|
|
1072
1228
|
toolCalls: sum(usages, (usage2) => usage2.toolCalls)
|
|
1073
1229
|
};
|
|
1074
1230
|
}
|
|
@@ -1160,11 +1316,17 @@ async function publishEvalReport(options) {
|
|
|
1160
1316
|
name: options.checkName,
|
|
1161
1317
|
repository: options.repository,
|
|
1162
1318
|
sha: options.sha,
|
|
1319
|
+
detailsUrl: options.detailsUrl,
|
|
1320
|
+
externalId: options.externalId,
|
|
1163
1321
|
token: options.token,
|
|
1164
1322
|
gate
|
|
1165
1323
|
});
|
|
1166
1324
|
if (checkRun.status === "skipped") {
|
|
1167
1325
|
options.warn?.(`GitHub Check Run skipped: ${checkRun.reason}`);
|
|
1326
|
+
} else if (checkRun.htmlUrl) {
|
|
1327
|
+
console.log(`published check run: ${checkRun.htmlUrl}`);
|
|
1328
|
+
} else if (checkRun.id !== void 0) {
|
|
1329
|
+
console.log(`published check run id: ${checkRun.id}`);
|
|
1168
1330
|
}
|
|
1169
1331
|
} catch (error) {
|
|
1170
1332
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1174,14 +1336,21 @@ async function publishEvalReport(options) {
|
|
|
1174
1336
|
options.warn?.(message);
|
|
1175
1337
|
}
|
|
1176
1338
|
}
|
|
1339
|
+
const gateFailed = gate.enforced && !gate.ok;
|
|
1340
|
+
const wantsSoftFail = options.softFail ?? options.checkRun === true;
|
|
1341
|
+
const softFail = wantsSoftFail && checkRunPublished(checkRun);
|
|
1342
|
+
const shouldFail = gateFailed && !softFail;
|
|
1177
1343
|
return {
|
|
1178
1344
|
report,
|
|
1179
1345
|
resultFiles,
|
|
1180
1346
|
gate,
|
|
1181
|
-
shouldFail
|
|
1347
|
+
shouldFail,
|
|
1182
1348
|
checkRun
|
|
1183
1349
|
};
|
|
1184
1350
|
}
|
|
1351
|
+
function checkRunPublished(checkRun) {
|
|
1352
|
+
return checkRun?.status === "created" || checkRun?.status === "updated";
|
|
1353
|
+
}
|
|
1185
1354
|
|
|
1186
1355
|
// src/cli.ts
|
|
1187
1356
|
main().catch((error) => {
|
|
@@ -1206,6 +1375,7 @@ async function main() {
|
|
|
1206
1375
|
checkName: options.checkName,
|
|
1207
1376
|
failOnCheckError: options.failOnCheckError,
|
|
1208
1377
|
failOnFailures: options.failOnFailures,
|
|
1378
|
+
softFail: options.softFail,
|
|
1209
1379
|
minPassRate: options.minPassRate,
|
|
1210
1380
|
minScoreAverage: options.minScoreAverage,
|
|
1211
1381
|
maxAnnotations: options.maxAnnotations,
|
|
@@ -1241,12 +1411,14 @@ function usage() {
|
|
|
1241
1411
|
" --fail-on-failures Exit non-zero when any eval case failed",
|
|
1242
1412
|
" --min-pass-rate <0-1> Exit non-zero when eval pass rate is below this floor",
|
|
1243
1413
|
" --min-score-average <0-1> Exit non-zero when average score is below this floor",
|
|
1414
|
+
" --soft-fail Keep exit 0 when a published Check Run owns gate status",
|
|
1415
|
+
" --no-soft-fail Always exit non-zero when an enforced gate fails",
|
|
1244
1416
|
" --fail-on-check-error Fail when Check Run publishing fails",
|
|
1245
1417
|
" --check-run-id <id> Update an existing Check Run",
|
|
1246
1418
|
" --check-name <name> Check Run name (default: vitest-evals)",
|
|
1247
1419
|
" --token <token> GitHub token (default: GITHUB_TOKEN)",
|
|
1248
1420
|
" --repo <owner/repo> GitHub repository (default: GITHUB_REPOSITORY)",
|
|
1249
|
-
" --sha <sha> Git commit SHA (default: GITHUB_SHA)",
|
|
1421
|
+
" --sha <sha> Git commit SHA (default: PR head, then GITHUB_SHA)",
|
|
1250
1422
|
" --workspace <path> Workspace path for relative annotation files",
|
|
1251
1423
|
" --max-annotations <n> Maximum annotations to emit",
|
|
1252
1424
|
" --max-failures <n> Maximum failures to include in details"
|