@vitest-evals/github-reporter 0.15.0 → 0.16.1
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 +37 -3
- package/dist/cli.js +303 -23
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +303 -23
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +125 -1
- package/dist/index.d.ts +125 -1
- package/dist/index.js +427 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +422 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -269,9 +269,10 @@ var DEFAULT_MAX_CHECK_ANNOTATIONS = 50;
|
|
|
269
269
|
var MAX_CHECK_FIELD_LENGTH = 64e3;
|
|
270
270
|
function renderWorkflowCommands(report, options = {}) {
|
|
271
271
|
const maxAnnotations = options.maxAnnotations ?? DEFAULT_MAX_WORKFLOW_ANNOTATIONS;
|
|
272
|
+
const command = caseAnnotationCommand(options.gate);
|
|
272
273
|
return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map(
|
|
273
274
|
(testCase) => formatWorkflowCommand({
|
|
274
|
-
command
|
|
275
|
+
command,
|
|
275
276
|
properties: {
|
|
276
277
|
file: testCase.displayFile,
|
|
277
278
|
line: String(testCase.location.line),
|
|
@@ -287,11 +288,12 @@ function buildCheckAnnotations(report, options = {}) {
|
|
|
287
288
|
options.maxAnnotations ?? DEFAULT_MAX_CHECK_ANNOTATIONS,
|
|
288
289
|
DEFAULT_MAX_CHECK_ANNOTATIONS
|
|
289
290
|
);
|
|
291
|
+
const annotationLevel = caseAnnotationLevel(options.gate);
|
|
290
292
|
return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map((testCase) => ({
|
|
291
293
|
path: testCase.displayFile,
|
|
292
294
|
start_line: testCase.location.line,
|
|
293
295
|
end_line: testCase.location.line,
|
|
294
|
-
annotation_level:
|
|
296
|
+
annotation_level: annotationLevel,
|
|
295
297
|
title: truncate(
|
|
296
298
|
`${testCase.primaryFailure?.judgeName ?? "vitest-evals"} - ${testCase.displayName}`,
|
|
297
299
|
255
|
|
@@ -303,6 +305,12 @@ function buildCheckAnnotations(report, options = {}) {
|
|
|
303
305
|
raw_details: truncate(formatRawDetails(testCase), MAX_CHECK_FIELD_LENGTH)
|
|
304
306
|
}));
|
|
305
307
|
}
|
|
308
|
+
function caseAnnotationCommand(gate) {
|
|
309
|
+
return gate?.ok ? "warning" : "error";
|
|
310
|
+
}
|
|
311
|
+
function caseAnnotationLevel(gate) {
|
|
312
|
+
return gate?.ok ? "warning" : "failure";
|
|
313
|
+
}
|
|
306
314
|
function hasAnnotationLocation(testCase) {
|
|
307
315
|
return Boolean(testCase.location);
|
|
308
316
|
}
|
|
@@ -362,6 +370,167 @@ function formatWorkflowCommand({
|
|
|
362
370
|
return `::${command} ${renderedProperties}::${escapeCommandData(message)}`;
|
|
363
371
|
}
|
|
364
372
|
|
|
373
|
+
// src/gate.ts
|
|
374
|
+
function evaluateEvalGate(report, policy = {}) {
|
|
375
|
+
const minPassRate = resolveMinPassRate(policy);
|
|
376
|
+
const minScoreAverage = policy.minScoreAverage;
|
|
377
|
+
const enforced = minPassRate !== void 0 || minScoreAverage !== void 0 || policy.failOnFailures === true;
|
|
378
|
+
const passRate = computePassRate(report);
|
|
379
|
+
const counts = formatEvalCounts(report, passRate);
|
|
380
|
+
if (!enforced) {
|
|
381
|
+
const ok = report.status === "passed";
|
|
382
|
+
return {
|
|
383
|
+
ok,
|
|
384
|
+
status: ok ? "passed" : "failed",
|
|
385
|
+
enforced: false,
|
|
386
|
+
passRate,
|
|
387
|
+
title: defaultCheckTitle(report),
|
|
388
|
+
message: ok ? `eval report passed: ${counts}` : `eval report failed: ${counts}`
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const nonEvalFailures = Math.max(
|
|
392
|
+
0,
|
|
393
|
+
report.totals.failed - report.totals.evalFailed
|
|
394
|
+
);
|
|
395
|
+
if (nonEvalFailures > 0) {
|
|
396
|
+
return {
|
|
397
|
+
ok: false,
|
|
398
|
+
status: "failed",
|
|
399
|
+
enforced: true,
|
|
400
|
+
passRate,
|
|
401
|
+
title: "Eval report hard failure",
|
|
402
|
+
message: `${formatNumber(nonEvalFailures)} non-eval test failure${nonEvalFailures === 1 ? "" : "s"}; ${counts}`
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
if (report.totals.evalTotal === 0) {
|
|
406
|
+
return {
|
|
407
|
+
ok: false,
|
|
408
|
+
status: "failed",
|
|
409
|
+
enforced: true,
|
|
410
|
+
passRate: null,
|
|
411
|
+
title: "Eval report hard failure",
|
|
412
|
+
message: "no eval cases were reported"
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (report.status === "failed" && report.totals.failed === 0 && report.failures.length === 0) {
|
|
416
|
+
return {
|
|
417
|
+
ok: false,
|
|
418
|
+
status: "failed",
|
|
419
|
+
enforced: true,
|
|
420
|
+
passRate,
|
|
421
|
+
title: "Eval report hard failure",
|
|
422
|
+
message: `vitest run failed without counted test failures; ${counts}`
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (minPassRate !== void 0 && (passRate === null || passRate + Number.EPSILON < minPassRate)) {
|
|
426
|
+
return {
|
|
427
|
+
ok: false,
|
|
428
|
+
status: "failed",
|
|
429
|
+
enforced: true,
|
|
430
|
+
passRate,
|
|
431
|
+
title: `Eval pass rate ${formatPercent(passRate)} \u2014 required ${formatPercent(minPassRate)}`,
|
|
432
|
+
message: `eval pass rate below floor: ${counts}; required >= ${formatPercent(minPassRate)}`
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (minScoreAverage !== void 0) {
|
|
436
|
+
const average = report.score?.average;
|
|
437
|
+
if (average === void 0 || !Number.isFinite(average)) {
|
|
438
|
+
return {
|
|
439
|
+
ok: false,
|
|
440
|
+
status: "failed",
|
|
441
|
+
enforced: true,
|
|
442
|
+
passRate,
|
|
443
|
+
title: "Eval score gate failed",
|
|
444
|
+
message: `no score average available; required avg score >= ${formatScore(minScoreAverage)}`
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (average + Number.EPSILON < minScoreAverage) {
|
|
448
|
+
return {
|
|
449
|
+
ok: false,
|
|
450
|
+
status: "failed",
|
|
451
|
+
enforced: true,
|
|
452
|
+
passRate,
|
|
453
|
+
title: `Avg score ${formatScore(average)} \u2014 required ${formatScore(minScoreAverage)}`,
|
|
454
|
+
message: `avg score below floor: ${counts}; required avg score >= ${formatScore(minScoreAverage)}`
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
ok: true,
|
|
460
|
+
status: "passed",
|
|
461
|
+
enforced: true,
|
|
462
|
+
passRate,
|
|
463
|
+
title: enforcedPassTitle(report, passRate, minPassRate, minScoreAverage),
|
|
464
|
+
message: `eval gate passed: ${counts}${formatFloorSuffix(minPassRate, minScoreAverage)}`
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function computePassRate(report) {
|
|
468
|
+
if (report.totals.evalTotal <= 0) {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
return report.totals.evalPassed / report.totals.evalTotal;
|
|
472
|
+
}
|
|
473
|
+
function formatPercent(value) {
|
|
474
|
+
if (value == null || !Number.isFinite(value)) {
|
|
475
|
+
return "n/a";
|
|
476
|
+
}
|
|
477
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
478
|
+
}
|
|
479
|
+
function renderGateWorkflowCommand(gate) {
|
|
480
|
+
if (gate.ok || !gate.enforced) {
|
|
481
|
+
return void 0;
|
|
482
|
+
}
|
|
483
|
+
return `::error title=${escapeCommandProperty(gate.title)}::${escapeCommandData(gate.message)}`;
|
|
484
|
+
}
|
|
485
|
+
function resolveMinPassRate(policy) {
|
|
486
|
+
const configured = policy.minPassRate;
|
|
487
|
+
if (policy.failOnFailures) {
|
|
488
|
+
if (configured === void 0) {
|
|
489
|
+
return 1;
|
|
490
|
+
}
|
|
491
|
+
return Math.max(configured, 1);
|
|
492
|
+
}
|
|
493
|
+
return configured;
|
|
494
|
+
}
|
|
495
|
+
function defaultCheckTitle(report) {
|
|
496
|
+
if (report.failures.length === 0 && report.status === "passed") {
|
|
497
|
+
return "No eval failures";
|
|
498
|
+
}
|
|
499
|
+
if (report.failures.length === 0) {
|
|
500
|
+
return "Vitest run failed";
|
|
501
|
+
}
|
|
502
|
+
return `${report.failures.length} eval failure${report.failures.length === 1 ? "" : "s"}`;
|
|
503
|
+
}
|
|
504
|
+
function enforcedPassTitle(report, passRate, minPassRate, minScoreAverage) {
|
|
505
|
+
if (minPassRate !== void 0) {
|
|
506
|
+
return `Eval pass rate ${formatPercent(passRate)} \u2014 floor ${formatPercent(minPassRate)}`;
|
|
507
|
+
}
|
|
508
|
+
if (minScoreAverage !== void 0) {
|
|
509
|
+
return `Avg score ${formatScore(report.score?.average)} \u2014 floor ${formatScore(minScoreAverage)}`;
|
|
510
|
+
}
|
|
511
|
+
return defaultCheckTitle(report);
|
|
512
|
+
}
|
|
513
|
+
function formatEvalCounts(report, passRate) {
|
|
514
|
+
const scoreText = report.score?.average === void 0 ? "n/a" : formatScore(report.score.average);
|
|
515
|
+
const passRateText = passRate === null ? "n/a" : formatPercent(passRate);
|
|
516
|
+
return `${formatNumber(report.totals.evalPassed)}/${formatNumber(
|
|
517
|
+
report.totals.evalTotal
|
|
518
|
+
)} passed (${passRateText}), avg score ${scoreText}`;
|
|
519
|
+
}
|
|
520
|
+
function formatFloorSuffix(minPassRate, minScoreAverage) {
|
|
521
|
+
const parts = [];
|
|
522
|
+
if (minPassRate !== void 0) {
|
|
523
|
+
parts.push(`pass rate floor ${formatPercent(minPassRate)}`);
|
|
524
|
+
}
|
|
525
|
+
if (minScoreAverage !== void 0) {
|
|
526
|
+
parts.push(`avg score floor ${formatScore(minScoreAverage)}`);
|
|
527
|
+
}
|
|
528
|
+
return parts.length === 0 ? "" : `; ${parts.join(", ")}`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/github.ts
|
|
532
|
+
import { readFileSync } from "fs";
|
|
533
|
+
|
|
365
534
|
// src/summary.ts
|
|
366
535
|
var DEFAULT_MAX_FAILURES = 20;
|
|
367
536
|
var DEFAULT_MAX_REASON_CHARS = 8e3;
|
|
@@ -382,20 +551,22 @@ function renderJobSummary(report, options = {}) {
|
|
|
382
551
|
const lines = [
|
|
383
552
|
"# vitest-evals",
|
|
384
553
|
"",
|
|
385
|
-
...renderSummaryTable(report, nonEvalFailures),
|
|
554
|
+
...renderSummaryTable(report, nonEvalFailures, options.gate),
|
|
386
555
|
"",
|
|
387
556
|
...renderScoreDistribution(report),
|
|
388
557
|
"## Results",
|
|
389
558
|
""
|
|
390
559
|
];
|
|
391
560
|
if (report.failures.length > 0) {
|
|
392
|
-
|
|
561
|
+
const failureHeading = options.gate?.ok === true ? "### Quality Misses" : "### Failures";
|
|
562
|
+
lines.push(failureHeading, "");
|
|
393
563
|
failures.forEach((testCase, index) => {
|
|
394
564
|
lines.push(...renderFailureDetails(testCase, index + 1, options), "");
|
|
395
565
|
});
|
|
396
566
|
if (report.failures.length > failures.length) {
|
|
567
|
+
const omittedLabel = options.gate?.ok === true ? "quality misses" : "failures";
|
|
397
568
|
lines.push(
|
|
398
|
-
`${report.failures.length - failures.length} more
|
|
569
|
+
`${report.failures.length - failures.length} more ${omittedLabel} omitted from this summary.`,
|
|
399
570
|
""
|
|
400
571
|
);
|
|
401
572
|
}
|
|
@@ -411,9 +582,9 @@ function renderJobSummary(report, options = {}) {
|
|
|
411
582
|
function formatCountLine(passed, failed, total) {
|
|
412
583
|
return `${formatNumber(passed)} passed, ${formatNumber(failed)} failed, ${formatNumber(total)} total`;
|
|
413
584
|
}
|
|
414
|
-
function renderSummaryTable(report, nonEvalFailures) {
|
|
585
|
+
function renderSummaryTable(report, nonEvalFailures, gate) {
|
|
415
586
|
const rows = [
|
|
416
|
-
["Status", report.status],
|
|
587
|
+
["Status", gate?.status ?? report.status],
|
|
417
588
|
[
|
|
418
589
|
"Evals",
|
|
419
590
|
formatCountLine(
|
|
@@ -423,9 +594,20 @@ function renderSummaryTable(report, nonEvalFailures) {
|
|
|
423
594
|
)
|
|
424
595
|
]
|
|
425
596
|
];
|
|
597
|
+
if (gate?.passRate !== void 0 && gate.passRate !== null) {
|
|
598
|
+
rows.push(["Pass Rate", formatPercent(gate.passRate)]);
|
|
599
|
+
} else if (report.totals.evalTotal > 0) {
|
|
600
|
+
rows.push([
|
|
601
|
+
"Pass Rate",
|
|
602
|
+
formatPercent(report.totals.evalPassed / report.totals.evalTotal)
|
|
603
|
+
]);
|
|
604
|
+
}
|
|
426
605
|
if (report.score) {
|
|
427
606
|
rows.push(["Score", formatScoreSummary(report.score)]);
|
|
428
607
|
}
|
|
608
|
+
if (gate?.enforced) {
|
|
609
|
+
rows.push(["Gate", gate.message]);
|
|
610
|
+
}
|
|
429
611
|
if (nonEvalFailures > 0) {
|
|
430
612
|
rows.push([
|
|
431
613
|
"Other Failures",
|
|
@@ -649,10 +831,44 @@ function formatCaseUsage(testCase) {
|
|
|
649
831
|
var DEFAULT_CHECK_NAME = "vitest-evals";
|
|
650
832
|
var MAX_CHECK_SUMMARY_LENGTH = 64e3;
|
|
651
833
|
var CHECK_SUMMARY_TRUNCATION_SUFFIX = "\n\n[truncated for GitHub Check Run]\n";
|
|
834
|
+
function resolveCheckSha(env = process.env, options = {}) {
|
|
835
|
+
const explicit = options.sha?.trim() || env.GITHUB_PR_HEAD_SHA?.trim();
|
|
836
|
+
if (explicit) {
|
|
837
|
+
return explicit;
|
|
838
|
+
}
|
|
839
|
+
const eventPath = options.eventPath?.trim() || env.GITHUB_EVENT_PATH?.trim();
|
|
840
|
+
if (eventPath) {
|
|
841
|
+
try {
|
|
842
|
+
const event = JSON.parse(readFileSync(eventPath, "utf8"));
|
|
843
|
+
const headSha = event.pull_request?.head?.sha;
|
|
844
|
+
if (typeof headSha === "string" && headSha.trim()) {
|
|
845
|
+
return headSha.trim();
|
|
846
|
+
}
|
|
847
|
+
} catch {
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return env.GITHUB_SHA?.trim() || void 0;
|
|
851
|
+
}
|
|
852
|
+
function resolveCheckDetailsUrl(env = process.env, options = {}) {
|
|
853
|
+
const explicit = options.detailsUrl?.trim();
|
|
854
|
+
if (explicit) {
|
|
855
|
+
return explicit;
|
|
856
|
+
}
|
|
857
|
+
const server = env.GITHUB_SERVER_URL?.replace(/\/$/, "");
|
|
858
|
+
const repository = env.GITHUB_REPOSITORY?.trim();
|
|
859
|
+
const runId = env.GITHUB_RUN_ID?.trim();
|
|
860
|
+
if (!server || !repository || !runId) {
|
|
861
|
+
return void 0;
|
|
862
|
+
}
|
|
863
|
+
return `${server}/${repository}/actions/runs/${runId}`;
|
|
864
|
+
}
|
|
652
865
|
async function publishCheckRun(report, options = {}) {
|
|
653
866
|
const token = options.token ?? process.env.GITHUB_TOKEN;
|
|
654
867
|
const repository = options.repository ?? process.env.GITHUB_REPOSITORY;
|
|
655
|
-
const sha = options.sha
|
|
868
|
+
const sha = resolveCheckSha(process.env, { sha: options.sha });
|
|
869
|
+
const detailsUrl = resolveCheckDetailsUrl(process.env, {
|
|
870
|
+
detailsUrl: options.detailsUrl
|
|
871
|
+
});
|
|
656
872
|
if (!token) {
|
|
657
873
|
return { status: "skipped", reason: "missing GITHUB_TOKEN" };
|
|
658
874
|
}
|
|
@@ -660,7 +876,10 @@ async function publishCheckRun(report, options = {}) {
|
|
|
660
876
|
return { status: "skipped", reason: "missing GITHUB_REPOSITORY" };
|
|
661
877
|
}
|
|
662
878
|
if (!sha && options.checkRunId === void 0) {
|
|
663
|
-
return {
|
|
879
|
+
return {
|
|
880
|
+
status: "skipped",
|
|
881
|
+
reason: "missing commit SHA (set --sha / options.sha, GITHUB_PR_HEAD_SHA, pull_request.head.sha, or GITHUB_SHA)"
|
|
882
|
+
};
|
|
664
883
|
}
|
|
665
884
|
const [owner, repo] = repository.split("/");
|
|
666
885
|
if (!owner || !repo) {
|
|
@@ -669,7 +888,7 @@ async function publishCheckRun(report, options = {}) {
|
|
|
669
888
|
reason: `invalid GitHub repository: ${repository}`
|
|
670
889
|
};
|
|
671
890
|
}
|
|
672
|
-
const payload = buildCheckRunPayload(report, options);
|
|
891
|
+
const payload = buildCheckRunPayload(report, options, detailsUrl);
|
|
673
892
|
const apiUrl = options.apiUrl ?? process.env.GITHUB_API_URL ?? "https://api.github.com";
|
|
674
893
|
const requestUrl = options.checkRunId === void 0 ? `${apiUrl}/repos/${owner}/${repo}/check-runs` : `${apiUrl}/repos/${owner}/${repo}/check-runs/${options.checkRunId}`;
|
|
675
894
|
const response = await fetch(requestUrl, {
|
|
@@ -684,6 +903,7 @@ async function publishCheckRun(report, options = {}) {
|
|
|
684
903
|
options.checkRunId === void 0 ? {
|
|
685
904
|
name: options.name ?? DEFAULT_CHECK_NAME,
|
|
686
905
|
head_sha: sha,
|
|
906
|
+
...options.externalId ? { external_id: options.externalId } : {},
|
|
687
907
|
...payload
|
|
688
908
|
} : payload
|
|
689
909
|
)
|
|
@@ -698,27 +918,31 @@ async function publishCheckRun(report, options = {}) {
|
|
|
698
918
|
return {
|
|
699
919
|
status: options.checkRunId === void 0 ? "created" : "updated",
|
|
700
920
|
id: data.id,
|
|
701
|
-
htmlUrl: data.html_url
|
|
921
|
+
htmlUrl: data.html_url,
|
|
922
|
+
sha
|
|
702
923
|
};
|
|
703
924
|
}
|
|
704
|
-
function buildCheckRunPayload(report, options) {
|
|
925
|
+
function buildCheckRunPayload(report, options, detailsUrl) {
|
|
926
|
+
const gate = options.gate ?? evaluateEvalGate(report);
|
|
705
927
|
const annotations = buildCheckAnnotations(report, {
|
|
706
|
-
maxAnnotations: options.maxAnnotations
|
|
928
|
+
maxAnnotations: options.maxAnnotations,
|
|
929
|
+
gate
|
|
707
930
|
});
|
|
708
|
-
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"}`;
|
|
709
931
|
return {
|
|
710
932
|
status: "completed",
|
|
711
|
-
conclusion:
|
|
933
|
+
conclusion: gate.ok ? "success" : "failure",
|
|
712
934
|
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
935
|
+
...detailsUrl ? { details_url: detailsUrl } : {},
|
|
713
936
|
output: {
|
|
714
|
-
title,
|
|
937
|
+
title: gate.title,
|
|
715
938
|
summary: truncateCheckSummary(
|
|
716
939
|
renderJobSummary(report, {
|
|
717
940
|
...options,
|
|
718
941
|
maxFailures: options.maxFailures ?? 5,
|
|
719
942
|
maxReasonChars: options.maxReasonChars ?? 4e3,
|
|
720
943
|
maxOutputChars: options.maxOutputChars ?? 2e3,
|
|
721
|
-
maxToolCalls: options.maxToolCalls ?? 10
|
|
944
|
+
maxToolCalls: options.maxToolCalls ?? 10,
|
|
945
|
+
gate
|
|
722
946
|
})
|
|
723
947
|
),
|
|
724
948
|
annotations
|
|
@@ -731,11 +955,191 @@ function truncateCheckSummary(summary) {
|
|
|
731
955
|
}
|
|
732
956
|
return `${summary.slice(0, MAX_CHECK_SUMMARY_LENGTH - CHECK_SUMMARY_TRUNCATION_SUFFIX.length).trimEnd()}${CHECK_SUMMARY_TRUNCATION_SUFFIX}`;
|
|
733
957
|
}
|
|
958
|
+
|
|
959
|
+
// src/report.ts
|
|
960
|
+
import { appendFile } from "fs/promises";
|
|
961
|
+
import {
|
|
962
|
+
readVitestJsonReportFile,
|
|
963
|
+
resolveResultFiles
|
|
964
|
+
} from "@vitest-evals/core/node";
|
|
965
|
+
|
|
966
|
+
// src/merge.ts
|
|
967
|
+
function mergeEvalReports(reports) {
|
|
968
|
+
const cases = reports.flatMap((report) => report.cases);
|
|
969
|
+
const failures = reports.flatMap((report) => report.failures);
|
|
970
|
+
const scoredCases = cases.map((testCase) => testCase.eval?.avgScore).filter(
|
|
971
|
+
(score) => typeof score === "number" && Number.isFinite(score)
|
|
972
|
+
);
|
|
973
|
+
const startedAtValues = reports.map((report) => report.startedAt).filter(
|
|
974
|
+
(startedAt2) => typeof startedAt2 === "number" && Number.isFinite(startedAt2)
|
|
975
|
+
);
|
|
976
|
+
const startedAt = startedAtValues.length > 0 ? Math.min(...startedAtValues) : void 0;
|
|
977
|
+
return {
|
|
978
|
+
status: reports.some((report) => report.status === "failed") ? "failed" : "passed",
|
|
979
|
+
startedAt,
|
|
980
|
+
durationMs: mergeDuration(reports),
|
|
981
|
+
totals: {
|
|
982
|
+
total: sum(reports, (report) => report.totals.total),
|
|
983
|
+
passed: sum(reports, (report) => report.totals.passed),
|
|
984
|
+
failed: sum(reports, (report) => report.totals.failed),
|
|
985
|
+
skipped: sum(reports, (report) => report.totals.skipped),
|
|
986
|
+
evalTotal: sum(reports, (report) => report.totals.evalTotal),
|
|
987
|
+
evalPassed: sum(reports, (report) => report.totals.evalPassed),
|
|
988
|
+
evalFailed: sum(reports, (report) => report.totals.evalFailed)
|
|
989
|
+
},
|
|
990
|
+
score: scoredCases.length > 0 ? {
|
|
991
|
+
average: scoredCases.reduce((total, score) => total + score, 0) / scoredCases.length,
|
|
992
|
+
minimum: Math.min(...scoredCases)
|
|
993
|
+
} : void 0,
|
|
994
|
+
usage: mergeUsage(reports.map((report) => report.usage)),
|
|
995
|
+
cases,
|
|
996
|
+
failures
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function mergeUsage(usages) {
|
|
1000
|
+
return {
|
|
1001
|
+
inputTokens: sum(usages, (usage) => usage.inputTokens),
|
|
1002
|
+
outputTokens: sum(usages, (usage) => usage.outputTokens),
|
|
1003
|
+
reasoningTokens: sum(usages, (usage) => usage.reasoningTokens),
|
|
1004
|
+
totalTokens: sum(usages, (usage) => usage.totalTokens),
|
|
1005
|
+
toolCalls: sum(usages, (usage) => usage.toolCalls)
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function mergeDuration(reports) {
|
|
1009
|
+
const durations = reports.map((report) => report.durationMs).filter(
|
|
1010
|
+
(durationMs) => typeof durationMs === "number" && Number.isFinite(durationMs)
|
|
1011
|
+
);
|
|
1012
|
+
const intervals = reports.map((report) => {
|
|
1013
|
+
if (typeof report.startedAt !== "number" || !Number.isFinite(report.startedAt) || typeof report.durationMs !== "number" || !Number.isFinite(report.durationMs)) {
|
|
1014
|
+
return void 0;
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
start: report.startedAt,
|
|
1018
|
+
end: report.startedAt + report.durationMs
|
|
1019
|
+
};
|
|
1020
|
+
}).filter(
|
|
1021
|
+
(interval) => Boolean(interval)
|
|
1022
|
+
);
|
|
1023
|
+
if (intervals.length > 0 && intervals.length === durations.length) {
|
|
1024
|
+
return Math.max(...intervals.map((interval) => interval.end)) - Math.min(...intervals.map((interval) => interval.start));
|
|
1025
|
+
}
|
|
1026
|
+
return durations.length > 0 ? durations.reduce((total, durationMs) => total + durationMs, 0) : void 0;
|
|
1027
|
+
}
|
|
1028
|
+
function sum(items, select) {
|
|
1029
|
+
return items.reduce((total, item) => total + select(item), 0);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/report.ts
|
|
1033
|
+
async function publishEvalReport(options) {
|
|
1034
|
+
const resultFiles = await resolveResultFiles(options.resultPatterns, {
|
|
1035
|
+
cwd: options.cwd
|
|
1036
|
+
});
|
|
1037
|
+
if (resultFiles.length === 0) {
|
|
1038
|
+
throw new Error(
|
|
1039
|
+
`No eval result files matched: ${options.resultPatterns.join(", ")}`
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
const reports = await Promise.all(
|
|
1043
|
+
resultFiles.map(async (resultFile) => {
|
|
1044
|
+
const json = await readVitestJsonReportFile(resultFile);
|
|
1045
|
+
return collectEvalReport(json, {
|
|
1046
|
+
workspace: options.workspace
|
|
1047
|
+
});
|
|
1048
|
+
})
|
|
1049
|
+
);
|
|
1050
|
+
const report = mergeEvalReports(reports);
|
|
1051
|
+
const gate = evaluateEvalGate(report, {
|
|
1052
|
+
failOnFailures: options.failOnFailures,
|
|
1053
|
+
minPassRate: options.minPassRate,
|
|
1054
|
+
minScoreAverage: options.minScoreAverage
|
|
1055
|
+
});
|
|
1056
|
+
const summary = renderJobSummary(report, {
|
|
1057
|
+
maxFailures: options.maxFailures,
|
|
1058
|
+
maxOutputChars: options.maxOutputChars,
|
|
1059
|
+
maxReasonChars: options.maxReasonChars,
|
|
1060
|
+
maxToolCalls: options.maxToolCalls,
|
|
1061
|
+
gate
|
|
1062
|
+
});
|
|
1063
|
+
if (options.summaryEnabled !== false) {
|
|
1064
|
+
if (options.summaryPath) {
|
|
1065
|
+
await appendFile(options.summaryPath, `${summary}
|
|
1066
|
+
`);
|
|
1067
|
+
} else {
|
|
1068
|
+
console.log(summary);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
if (options.annotations) {
|
|
1072
|
+
const gateCommand = renderGateWorkflowCommand(gate);
|
|
1073
|
+
if (gateCommand) {
|
|
1074
|
+
console.log(gateCommand);
|
|
1075
|
+
}
|
|
1076
|
+
for (const command of renderWorkflowCommands(report, {
|
|
1077
|
+
maxAnnotations: options.maxAnnotations,
|
|
1078
|
+
gate
|
|
1079
|
+
})) {
|
|
1080
|
+
console.log(command);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
let checkRun;
|
|
1084
|
+
if (options.checkRun) {
|
|
1085
|
+
try {
|
|
1086
|
+
checkRun = await publishCheckRun(report, {
|
|
1087
|
+
checkRunId: options.checkRunId,
|
|
1088
|
+
maxAnnotations: options.maxAnnotations,
|
|
1089
|
+
maxFailures: options.maxFailures,
|
|
1090
|
+
maxOutputChars: options.maxOutputChars,
|
|
1091
|
+
maxReasonChars: options.maxReasonChars,
|
|
1092
|
+
maxToolCalls: options.maxToolCalls,
|
|
1093
|
+
name: options.checkName,
|
|
1094
|
+
repository: options.repository,
|
|
1095
|
+
sha: options.sha,
|
|
1096
|
+
detailsUrl: options.detailsUrl,
|
|
1097
|
+
externalId: options.externalId,
|
|
1098
|
+
token: options.token,
|
|
1099
|
+
gate
|
|
1100
|
+
});
|
|
1101
|
+
if (checkRun.status === "skipped") {
|
|
1102
|
+
options.warn?.(`GitHub Check Run skipped: ${checkRun.reason}`);
|
|
1103
|
+
} else if (checkRun.htmlUrl) {
|
|
1104
|
+
console.log(`published check run: ${checkRun.htmlUrl}`);
|
|
1105
|
+
} else if (checkRun.id !== void 0) {
|
|
1106
|
+
console.log(`published check run id: ${checkRun.id}`);
|
|
1107
|
+
}
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1110
|
+
if (options.failOnCheckError) {
|
|
1111
|
+
throw error;
|
|
1112
|
+
}
|
|
1113
|
+
options.warn?.(message);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
const gateFailed = gate.enforced && !gate.ok;
|
|
1117
|
+
const wantsSoftFail = options.softFail ?? options.checkRun === true;
|
|
1118
|
+
const softFail = wantsSoftFail && checkRunPublished(checkRun);
|
|
1119
|
+
const shouldFail = gateFailed && !softFail;
|
|
1120
|
+
return {
|
|
1121
|
+
report,
|
|
1122
|
+
resultFiles,
|
|
1123
|
+
gate,
|
|
1124
|
+
shouldFail,
|
|
1125
|
+
checkRun
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
function checkRunPublished(checkRun) {
|
|
1129
|
+
return checkRun?.status === "created" || checkRun?.status === "updated";
|
|
1130
|
+
}
|
|
734
1131
|
export {
|
|
735
1132
|
buildCheckAnnotations,
|
|
736
1133
|
collectEvalReport,
|
|
1134
|
+
computePassRate,
|
|
1135
|
+
evaluateEvalGate,
|
|
1136
|
+
formatPercent,
|
|
737
1137
|
publishCheckRun,
|
|
1138
|
+
publishEvalReport,
|
|
1139
|
+
renderGateWorkflowCommand,
|
|
738
1140
|
renderJobSummary,
|
|
739
|
-
renderWorkflowCommands
|
|
1141
|
+
renderWorkflowCommands,
|
|
1142
|
+
resolveCheckDetailsUrl,
|
|
1143
|
+
resolveCheckSha
|
|
740
1144
|
};
|
|
741
1145
|
//# sourceMappingURL=index.mjs.map
|