@vitest-evals/github-reporter 0.15.0 → 0.16.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 +22 -1
- package/dist/cli.js +232 -17
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +232 -17
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +94 -1
- package/dist/index.d.ts +94 -1
- package/dist/index.js +360 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +358 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -40,6 +40,12 @@ function parseCliArgs(args, env = process.env) {
|
|
|
40
40
|
case "--fail-on-check-error":
|
|
41
41
|
options.failOnCheckError = true;
|
|
42
42
|
break;
|
|
43
|
+
case "--min-pass-rate":
|
|
44
|
+
options.minPassRate = readRatio(args, ++index, arg);
|
|
45
|
+
break;
|
|
46
|
+
case "--min-score-average":
|
|
47
|
+
options.minScoreAverage = readRatio(args, ++index, arg);
|
|
48
|
+
break;
|
|
43
49
|
case "--max-annotations":
|
|
44
50
|
options.maxAnnotations = readInteger(args, ++index, arg);
|
|
45
51
|
break;
|
|
@@ -99,6 +105,14 @@ function readInteger(args, index, flag) {
|
|
|
99
105
|
}
|
|
100
106
|
return Number(rawValue);
|
|
101
107
|
}
|
|
108
|
+
function readRatio(args, index, flag) {
|
|
109
|
+
const rawValue = readValue(args, index, flag);
|
|
110
|
+
const parsed = Number(rawValue);
|
|
111
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
|
|
112
|
+
throw new Error(`Invalid ratio for ${flag}`);
|
|
113
|
+
}
|
|
114
|
+
return parsed;
|
|
115
|
+
}
|
|
102
116
|
|
|
103
117
|
// src/report.ts
|
|
104
118
|
import { appendFile } from "fs/promises";
|
|
@@ -190,9 +204,10 @@ var DEFAULT_MAX_CHECK_ANNOTATIONS = 50;
|
|
|
190
204
|
var MAX_CHECK_FIELD_LENGTH = 64e3;
|
|
191
205
|
function renderWorkflowCommands(report, options = {}) {
|
|
192
206
|
const maxAnnotations = options.maxAnnotations ?? DEFAULT_MAX_WORKFLOW_ANNOTATIONS;
|
|
207
|
+
const command = caseAnnotationCommand(options.gate);
|
|
193
208
|
return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map(
|
|
194
209
|
(testCase) => formatWorkflowCommand({
|
|
195
|
-
command
|
|
210
|
+
command,
|
|
196
211
|
properties: {
|
|
197
212
|
file: testCase.displayFile,
|
|
198
213
|
line: String(testCase.location.line),
|
|
@@ -208,11 +223,12 @@ function buildCheckAnnotations(report, options = {}) {
|
|
|
208
223
|
options.maxAnnotations ?? DEFAULT_MAX_CHECK_ANNOTATIONS,
|
|
209
224
|
DEFAULT_MAX_CHECK_ANNOTATIONS
|
|
210
225
|
);
|
|
226
|
+
const annotationLevel = caseAnnotationLevel(options.gate);
|
|
211
227
|
return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map((testCase) => ({
|
|
212
228
|
path: testCase.displayFile,
|
|
213
229
|
start_line: testCase.location.line,
|
|
214
230
|
end_line: testCase.location.line,
|
|
215
|
-
annotation_level:
|
|
231
|
+
annotation_level: annotationLevel,
|
|
216
232
|
title: truncate(
|
|
217
233
|
`${testCase.primaryFailure?.judgeName ?? "vitest-evals"} - ${testCase.displayName}`,
|
|
218
234
|
255
|
|
@@ -224,6 +240,12 @@ function buildCheckAnnotations(report, options = {}) {
|
|
|
224
240
|
raw_details: truncate(formatRawDetails(testCase), MAX_CHECK_FIELD_LENGTH)
|
|
225
241
|
}));
|
|
226
242
|
}
|
|
243
|
+
function caseAnnotationCommand(gate) {
|
|
244
|
+
return gate?.ok ? "warning" : "error";
|
|
245
|
+
}
|
|
246
|
+
function caseAnnotationLevel(gate) {
|
|
247
|
+
return gate?.ok ? "warning" : "failure";
|
|
248
|
+
}
|
|
227
249
|
function hasAnnotationLocation(testCase) {
|
|
228
250
|
return Boolean(testCase.location);
|
|
229
251
|
}
|
|
@@ -469,6 +491,164 @@ function toolCallCount(testCase) {
|
|
|
469
491
|
return testCase.toolCalls.length;
|
|
470
492
|
}
|
|
471
493
|
|
|
494
|
+
// src/gate.ts
|
|
495
|
+
function evaluateEvalGate(report, policy = {}) {
|
|
496
|
+
const minPassRate = resolveMinPassRate(policy);
|
|
497
|
+
const minScoreAverage = policy.minScoreAverage;
|
|
498
|
+
const enforced = minPassRate !== void 0 || minScoreAverage !== void 0 || policy.failOnFailures === true;
|
|
499
|
+
const passRate = computePassRate(report);
|
|
500
|
+
const counts = formatEvalCounts(report, passRate);
|
|
501
|
+
if (!enforced) {
|
|
502
|
+
const ok = report.status === "passed";
|
|
503
|
+
return {
|
|
504
|
+
ok,
|
|
505
|
+
status: ok ? "passed" : "failed",
|
|
506
|
+
enforced: false,
|
|
507
|
+
passRate,
|
|
508
|
+
title: defaultCheckTitle(report),
|
|
509
|
+
message: ok ? `eval report passed: ${counts}` : `eval report failed: ${counts}`
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
const nonEvalFailures = Math.max(
|
|
513
|
+
0,
|
|
514
|
+
report.totals.failed - report.totals.evalFailed
|
|
515
|
+
);
|
|
516
|
+
if (nonEvalFailures > 0) {
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
status: "failed",
|
|
520
|
+
enforced: true,
|
|
521
|
+
passRate,
|
|
522
|
+
title: "Eval report hard failure",
|
|
523
|
+
message: `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}; ${counts}`
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
if (report.totals.evalTotal === 0) {
|
|
527
|
+
return {
|
|
528
|
+
ok: false,
|
|
529
|
+
status: "failed",
|
|
530
|
+
enforced: true,
|
|
531
|
+
passRate: null,
|
|
532
|
+
title: "Eval report hard failure",
|
|
533
|
+
message: "no eval cases were reported"
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
if (report.status === "failed" && report.totals.failed === 0 && report.failures.length === 0) {
|
|
537
|
+
return {
|
|
538
|
+
ok: false,
|
|
539
|
+
status: "failed",
|
|
540
|
+
enforced: true,
|
|
541
|
+
passRate,
|
|
542
|
+
title: "Eval report hard failure",
|
|
543
|
+
message: `vitest run failed without counted test failures; ${counts}`
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
if (minPassRate !== void 0 && (passRate === null || passRate + Number.EPSILON < minPassRate)) {
|
|
547
|
+
return {
|
|
548
|
+
ok: false,
|
|
549
|
+
status: "failed",
|
|
550
|
+
enforced: true,
|
|
551
|
+
passRate,
|
|
552
|
+
title: `Eval pass rate ${formatPercent(passRate)} \u2014 required ${formatPercent(minPassRate)}`,
|
|
553
|
+
message: `eval pass rate below floor: ${counts}; required >= ${formatPercent(minPassRate)}`
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
if (minScoreAverage !== void 0) {
|
|
557
|
+
const average = report.score?.average;
|
|
558
|
+
if (average === void 0 || !Number.isFinite(average)) {
|
|
559
|
+
return {
|
|
560
|
+
ok: false,
|
|
561
|
+
status: "failed",
|
|
562
|
+
enforced: true,
|
|
563
|
+
passRate,
|
|
564
|
+
title: "Eval score gate failed",
|
|
565
|
+
message: `no score average available; required avg score >= ${formatScore(minScoreAverage)}`
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
if (average + Number.EPSILON < minScoreAverage) {
|
|
569
|
+
return {
|
|
570
|
+
ok: false,
|
|
571
|
+
status: "failed",
|
|
572
|
+
enforced: true,
|
|
573
|
+
passRate,
|
|
574
|
+
title: `Avg score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
|
|
575
|
+
message: `avg score below floor: ${counts}; required avg score >= ${formatScore(minScoreAverage)}`
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
ok: true,
|
|
581
|
+
status: "passed",
|
|
582
|
+
enforced: true,
|
|
583
|
+
passRate,
|
|
584
|
+
title: enforcedPassTitle(report, passRate, minPassRate, minScoreAverage),
|
|
585
|
+
message: `eval gate passed: ${counts}${formatFloorSuffix(minPassRate, minScoreAverage)}`
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function computePassRate(report) {
|
|
589
|
+
if (report.totals.evalTotal <= 0) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
return report.totals.evalPassed / report.totals.evalTotal;
|
|
593
|
+
}
|
|
594
|
+
function formatPercent(value) {
|
|
595
|
+
if (value == null || !Number.isFinite(value)) {
|
|
596
|
+
return "n/a";
|
|
597
|
+
}
|
|
598
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
599
|
+
}
|
|
600
|
+
function renderGateWorkflowCommand(gate) {
|
|
601
|
+
if (gate.ok || !gate.enforced) {
|
|
602
|
+
return void 0;
|
|
603
|
+
}
|
|
604
|
+
return `::error title=${escapeCommandProperty(gate.title)}::${escapeCommandData(gate.message)}`;
|
|
605
|
+
}
|
|
606
|
+
function resolveMinPassRate(policy) {
|
|
607
|
+
const configured = policy.minPassRate;
|
|
608
|
+
if (policy.failOnFailures) {
|
|
609
|
+
if (configured === void 0) {
|
|
610
|
+
return 1;
|
|
611
|
+
}
|
|
612
|
+
return Math.max(configured, 1);
|
|
613
|
+
}
|
|
614
|
+
return configured;
|
|
615
|
+
}
|
|
616
|
+
function defaultCheckTitle(report) {
|
|
617
|
+
if (report.failures.length === 0 && report.status === "passed") {
|
|
618
|
+
return "No eval failures";
|
|
619
|
+
}
|
|
620
|
+
if (report.failures.length === 0) {
|
|
621
|
+
return "Vitest run failed";
|
|
622
|
+
}
|
|
623
|
+
return `${report.failures.length} eval failure${report.failures.length === 1 ? "" : "s"}`;
|
|
624
|
+
}
|
|
625
|
+
function enforcedPassTitle(report, passRate, minPassRate, minScoreAverage) {
|
|
626
|
+
if (minPassRate !== void 0) {
|
|
627
|
+
return `Eval pass rate ${formatPercent(passRate)} \u2014 floor ${formatPercent(minPassRate)}`;
|
|
628
|
+
}
|
|
629
|
+
if (minScoreAverage !== void 0) {
|
|
630
|
+
return `Avg score ${formatScore(report.score?.average)} \u2014 floor ${formatScore(minScoreAverage)}`;
|
|
631
|
+
}
|
|
632
|
+
return defaultCheckTitle(report);
|
|
633
|
+
}
|
|
634
|
+
function formatEvalCounts(report, passRate) {
|
|
635
|
+
const scoreText = report.score?.average === void 0 ? "n/a" : formatScore(report.score.average);
|
|
636
|
+
const passRateText = passRate === null ? "n/a" : formatPercent(passRate);
|
|
637
|
+
return `${formatNumber(report.totals.evalPassed)}/${formatNumber(
|
|
638
|
+
report.totals.evalTotal
|
|
639
|
+
)} passed (${passRateText}), avg score ${scoreText}`;
|
|
640
|
+
}
|
|
641
|
+
function formatFloorSuffix(minPassRate, minScoreAverage) {
|
|
642
|
+
const parts = [];
|
|
643
|
+
if (minPassRate !== void 0) {
|
|
644
|
+
parts.push(`pass rate floor ${formatPercent(minPassRate)}`);
|
|
645
|
+
}
|
|
646
|
+
if (minScoreAverage !== void 0) {
|
|
647
|
+
parts.push(`avg score floor ${formatScore(minScoreAverage)}`);
|
|
648
|
+
}
|
|
649
|
+
return parts.length === 0 ? "" : `; ${parts.join(", ")}`;
|
|
650
|
+
}
|
|
651
|
+
|
|
472
652
|
// src/summary.ts
|
|
473
653
|
var DEFAULT_MAX_FAILURES = 20;
|
|
474
654
|
var DEFAULT_MAX_REASON_CHARS = 8e3;
|
|
@@ -489,20 +669,22 @@ function renderJobSummary(report, options = {}) {
|
|
|
489
669
|
const lines = [
|
|
490
670
|
"# vitest-evals",
|
|
491
671
|
"",
|
|
492
|
-
...renderSummaryTable(report, nonEvalFailures),
|
|
672
|
+
...renderSummaryTable(report, nonEvalFailures, options.gate),
|
|
493
673
|
"",
|
|
494
674
|
...renderScoreDistribution(report),
|
|
495
675
|
"## Results",
|
|
496
676
|
""
|
|
497
677
|
];
|
|
498
678
|
if (report.failures.length > 0) {
|
|
499
|
-
|
|
679
|
+
const failureHeading = options.gate?.ok === true ? "### Quality Misses" : "### Failures";
|
|
680
|
+
lines.push(failureHeading, "");
|
|
500
681
|
failures.forEach((testCase, index) => {
|
|
501
682
|
lines.push(...renderFailureDetails(testCase, index + 1, options), "");
|
|
502
683
|
});
|
|
503
684
|
if (report.failures.length > failures.length) {
|
|
685
|
+
const omittedLabel = options.gate?.ok === true ? "quality misses" : "failures";
|
|
504
686
|
lines.push(
|
|
505
|
-
`${report.failures.length - failures.length} more
|
|
687
|
+
`${report.failures.length - failures.length} more ${omittedLabel} omitted from this summary.`,
|
|
506
688
|
""
|
|
507
689
|
);
|
|
508
690
|
}
|
|
@@ -518,9 +700,9 @@ function renderJobSummary(report, options = {}) {
|
|
|
518
700
|
function formatCountLine(passed, failed, total) {
|
|
519
701
|
return `${formatNumber(passed)} passed, ${formatNumber(failed)} failed, ${formatNumber(total)} total`;
|
|
520
702
|
}
|
|
521
|
-
function renderSummaryTable(report, nonEvalFailures) {
|
|
703
|
+
function renderSummaryTable(report, nonEvalFailures, gate) {
|
|
522
704
|
const rows = [
|
|
523
|
-
["Status", report.status],
|
|
705
|
+
["Status", gate?.status ?? report.status],
|
|
524
706
|
[
|
|
525
707
|
"Evals",
|
|
526
708
|
formatCountLine(
|
|
@@ -530,9 +712,20 @@ function renderSummaryTable(report, nonEvalFailures) {
|
|
|
530
712
|
)
|
|
531
713
|
]
|
|
532
714
|
];
|
|
715
|
+
if (gate?.passRate !== void 0 && gate.passRate !== null) {
|
|
716
|
+
rows.push(["Pass Rate", formatPercent(gate.passRate)]);
|
|
717
|
+
} else if (report.totals.evalTotal > 0) {
|
|
718
|
+
rows.push([
|
|
719
|
+
"Pass Rate",
|
|
720
|
+
formatPercent(report.totals.evalPassed / report.totals.evalTotal)
|
|
721
|
+
]);
|
|
722
|
+
}
|
|
533
723
|
if (report.score) {
|
|
534
724
|
rows.push(["Score", formatScoreSummary(report.score)]);
|
|
535
725
|
}
|
|
726
|
+
if (gate?.enforced) {
|
|
727
|
+
rows.push(["Gate", gate.message]);
|
|
728
|
+
}
|
|
536
729
|
if (nonEvalFailures > 0) {
|
|
537
730
|
rows.push([
|
|
538
731
|
"Other Failures",
|
|
@@ -809,23 +1002,25 @@ async function publishCheckRun(report, options = {}) {
|
|
|
809
1002
|
};
|
|
810
1003
|
}
|
|
811
1004
|
function buildCheckRunPayload(report, options) {
|
|
1005
|
+
const gate = options.gate ?? evaluateEvalGate(report);
|
|
812
1006
|
const annotations = buildCheckAnnotations(report, {
|
|
813
|
-
maxAnnotations: options.maxAnnotations
|
|
1007
|
+
maxAnnotations: options.maxAnnotations,
|
|
1008
|
+
gate
|
|
814
1009
|
});
|
|
815
|
-
const title = report.failures.length === 0 && report.status === "passed" ? "No eval failures" : report.failures.length === 0 ? "Vitest run failed" : `${report.failures.length} eval failure${report.failures.length === 1 ? "" : "s"}`;
|
|
816
1010
|
return {
|
|
817
1011
|
status: "completed",
|
|
818
|
-
conclusion:
|
|
1012
|
+
conclusion: gate.ok ? "success" : "failure",
|
|
819
1013
|
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
820
1014
|
output: {
|
|
821
|
-
title,
|
|
1015
|
+
title: gate.title,
|
|
822
1016
|
summary: truncateCheckSummary(
|
|
823
1017
|
renderJobSummary(report, {
|
|
824
1018
|
...options,
|
|
825
1019
|
maxFailures: options.maxFailures ?? 5,
|
|
826
1020
|
maxReasonChars: options.maxReasonChars ?? 4e3,
|
|
827
1021
|
maxOutputChars: options.maxOutputChars ?? 2e3,
|
|
828
|
-
maxToolCalls: options.maxToolCalls ?? 10
|
|
1022
|
+
maxToolCalls: options.maxToolCalls ?? 10,
|
|
1023
|
+
gate
|
|
829
1024
|
})
|
|
830
1025
|
),
|
|
831
1026
|
annotations
|
|
@@ -924,11 +1119,17 @@ async function publishEvalReport(options) {
|
|
|
924
1119
|
})
|
|
925
1120
|
);
|
|
926
1121
|
const report = mergeEvalReports(reports);
|
|
1122
|
+
const gate = evaluateEvalGate(report, {
|
|
1123
|
+
failOnFailures: options.failOnFailures,
|
|
1124
|
+
minPassRate: options.minPassRate,
|
|
1125
|
+
minScoreAverage: options.minScoreAverage
|
|
1126
|
+
});
|
|
927
1127
|
const summary = renderJobSummary(report, {
|
|
928
1128
|
maxFailures: options.maxFailures,
|
|
929
1129
|
maxOutputChars: options.maxOutputChars,
|
|
930
1130
|
maxReasonChars: options.maxReasonChars,
|
|
931
|
-
maxToolCalls: options.maxToolCalls
|
|
1131
|
+
maxToolCalls: options.maxToolCalls,
|
|
1132
|
+
gate
|
|
932
1133
|
});
|
|
933
1134
|
if (options.summaryEnabled !== false) {
|
|
934
1135
|
if (options.summaryPath) {
|
|
@@ -939,8 +1140,13 @@ async function publishEvalReport(options) {
|
|
|
939
1140
|
}
|
|
940
1141
|
}
|
|
941
1142
|
if (options.annotations) {
|
|
1143
|
+
const gateCommand = renderGateWorkflowCommand(gate);
|
|
1144
|
+
if (gateCommand) {
|
|
1145
|
+
console.log(gateCommand);
|
|
1146
|
+
}
|
|
942
1147
|
for (const command of renderWorkflowCommands(report, {
|
|
943
|
-
maxAnnotations: options.maxAnnotations
|
|
1148
|
+
maxAnnotations: options.maxAnnotations,
|
|
1149
|
+
gate
|
|
944
1150
|
})) {
|
|
945
1151
|
console.log(command);
|
|
946
1152
|
}
|
|
@@ -958,7 +1164,8 @@ async function publishEvalReport(options) {
|
|
|
958
1164
|
name: options.checkName,
|
|
959
1165
|
repository: options.repository,
|
|
960
1166
|
sha: options.sha,
|
|
961
|
-
token: options.token
|
|
1167
|
+
token: options.token,
|
|
1168
|
+
gate
|
|
962
1169
|
});
|
|
963
1170
|
if (checkRun.status === "skipped") {
|
|
964
1171
|
options.warn?.(`GitHub Check Run skipped: ${checkRun.reason}`);
|
|
@@ -974,6 +1181,8 @@ async function publishEvalReport(options) {
|
|
|
974
1181
|
return {
|
|
975
1182
|
report,
|
|
976
1183
|
resultFiles,
|
|
1184
|
+
gate,
|
|
1185
|
+
shouldFail: gate.enforced && !gate.ok,
|
|
977
1186
|
checkRun
|
|
978
1187
|
};
|
|
979
1188
|
}
|
|
@@ -1000,6 +1209,9 @@ async function main() {
|
|
|
1000
1209
|
checkRunId: options.checkRunId,
|
|
1001
1210
|
checkName: options.checkName,
|
|
1002
1211
|
failOnCheckError: options.failOnCheckError,
|
|
1212
|
+
failOnFailures: options.failOnFailures,
|
|
1213
|
+
minPassRate: options.minPassRate,
|
|
1214
|
+
minScoreAverage: options.minScoreAverage,
|
|
1003
1215
|
maxAnnotations: options.maxAnnotations,
|
|
1004
1216
|
maxFailures: options.maxFailures,
|
|
1005
1217
|
repository: options.repository,
|
|
@@ -1007,7 +1219,8 @@ async function main() {
|
|
|
1007
1219
|
token: options.token,
|
|
1008
1220
|
warn
|
|
1009
1221
|
});
|
|
1010
|
-
if (
|
|
1222
|
+
if (result.shouldFail) {
|
|
1223
|
+
console.error(result.gate.message);
|
|
1011
1224
|
process.exitCode = 1;
|
|
1012
1225
|
}
|
|
1013
1226
|
}
|
|
@@ -1029,7 +1242,9 @@ function usage() {
|
|
|
1029
1242
|
" --annotations Emit GitHub workflow-command annotations",
|
|
1030
1243
|
" --no-annotations Disable workflow-command annotations",
|
|
1031
1244
|
" --check-run Publish a GitHub Check Run when configured",
|
|
1032
|
-
" --fail-on-failures Exit non-zero when
|
|
1245
|
+
" --fail-on-failures Exit non-zero when any eval case failed",
|
|
1246
|
+
" --min-pass-rate <0-1> Exit non-zero when eval pass rate is below this floor",
|
|
1247
|
+
" --min-score-average <0-1> Exit non-zero when average score is below this floor",
|
|
1033
1248
|
" --fail-on-check-error Fail when Check Run publishing fails",
|
|
1034
1249
|
" --check-run-id <id> Update an existing Check Run",
|
|
1035
1250
|
" --check-name <name> Check Run name (default: vitest-evals)",
|