@vitest-evals/github-reporter 0.9.0-beta.5 → 0.9.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 +54 -66
- package/dist/cli.js +367 -153
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +366 -152
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +1 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +5 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -32
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -2
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
-
// src/cli.ts
|
|
5
|
-
var import_promises = require("fs/promises");
|
|
6
|
-
|
|
7
4
|
// src/cli-options.ts
|
|
8
5
|
function parseCliArgs(args, env = process.env) {
|
|
9
6
|
const options = {
|
|
@@ -11,14 +8,16 @@ function parseCliArgs(args, env = process.env) {
|
|
|
11
8
|
summaryEnabled: true,
|
|
12
9
|
annotations: env.GITHUB_ACTIONS === "true",
|
|
13
10
|
checkRun: false,
|
|
11
|
+
failOnFailures: false,
|
|
14
12
|
failOnCheckError: false,
|
|
13
|
+
resultPatterns: [],
|
|
15
14
|
help: false
|
|
16
15
|
};
|
|
17
16
|
for (let index = 0; index < args.length; index += 1) {
|
|
18
17
|
const arg = args[index];
|
|
19
18
|
switch (arg) {
|
|
20
19
|
case "--json":
|
|
21
|
-
options.
|
|
20
|
+
options.resultPatterns.push(readValue(args, ++index, arg));
|
|
22
21
|
break;
|
|
23
22
|
case "--summary":
|
|
24
23
|
options.summaryPath = readValue(args, ++index, arg);
|
|
@@ -36,6 +35,9 @@ function parseCliArgs(args, env = process.env) {
|
|
|
36
35
|
case "--check-run":
|
|
37
36
|
options.checkRun = true;
|
|
38
37
|
break;
|
|
38
|
+
case "--fail-on-failures":
|
|
39
|
+
options.failOnFailures = true;
|
|
40
|
+
break;
|
|
39
41
|
case "--fail-on-check-error":
|
|
40
42
|
options.failOnCheckError = true;
|
|
41
43
|
break;
|
|
@@ -69,8 +71,8 @@ function parseCliArgs(args, env = process.env) {
|
|
|
69
71
|
options.help = true;
|
|
70
72
|
return withDefaultJsonPath(options, env);
|
|
71
73
|
default:
|
|
72
|
-
if (!arg.startsWith("-")
|
|
73
|
-
options.
|
|
74
|
+
if (!arg.startsWith("-")) {
|
|
75
|
+
options.resultPatterns.push(arg);
|
|
74
76
|
break;
|
|
75
77
|
}
|
|
76
78
|
throw new Error(`Unknown argument: ${arg}`);
|
|
@@ -81,7 +83,7 @@ function parseCliArgs(args, env = process.env) {
|
|
|
81
83
|
function withDefaultJsonPath(options, env) {
|
|
82
84
|
return {
|
|
83
85
|
...options,
|
|
84
|
-
|
|
86
|
+
resultPatterns: options.resultPatterns.length > 0 ? options.resultPatterns : [env.VITEST_EVALS_JSON_REPORT || "vitest-results.json"]
|
|
85
87
|
};
|
|
86
88
|
}
|
|
87
89
|
function readValue(args, index, flag) {
|
|
@@ -92,13 +94,16 @@ function readValue(args, index, flag) {
|
|
|
92
94
|
return value;
|
|
93
95
|
}
|
|
94
96
|
function readInteger(args, index, flag) {
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
+
const rawValue = readValue(args, index, flag);
|
|
98
|
+
if (!/^\d+$/.test(rawValue)) {
|
|
97
99
|
throw new Error(`Invalid integer for ${flag}`);
|
|
98
100
|
}
|
|
99
|
-
return
|
|
101
|
+
return Number(rawValue);
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
// src/report.ts
|
|
105
|
+
var import_promises2 = require("fs/promises");
|
|
106
|
+
|
|
102
107
|
// src/utils.ts
|
|
103
108
|
var import_node_path = require("path");
|
|
104
109
|
function isRecord(value) {
|
|
@@ -190,6 +195,98 @@ function escapeCommandProperty(value) {
|
|
|
190
195
|
return escapeCommandData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
191
196
|
}
|
|
192
197
|
|
|
198
|
+
// src/annotations.ts
|
|
199
|
+
var DEFAULT_MAX_WORKFLOW_ANNOTATIONS = 10;
|
|
200
|
+
var DEFAULT_MAX_CHECK_ANNOTATIONS = 50;
|
|
201
|
+
var MAX_CHECK_FIELD_LENGTH = 64e3;
|
|
202
|
+
function renderWorkflowCommands(report, options = {}) {
|
|
203
|
+
const maxAnnotations = options.maxAnnotations ?? DEFAULT_MAX_WORKFLOW_ANNOTATIONS;
|
|
204
|
+
return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map(
|
|
205
|
+
(testCase) => formatWorkflowCommand({
|
|
206
|
+
command: "error",
|
|
207
|
+
properties: {
|
|
208
|
+
file: testCase.displayFile,
|
|
209
|
+
line: String(testCase.location.line),
|
|
210
|
+
col: String(testCase.location.column),
|
|
211
|
+
title: "vitest-evals"
|
|
212
|
+
},
|
|
213
|
+
message: formatWorkflowMessage(testCase)
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
function buildCheckAnnotations(report, options = {}) {
|
|
218
|
+
const maxAnnotations = Math.min(
|
|
219
|
+
options.maxAnnotations ?? DEFAULT_MAX_CHECK_ANNOTATIONS,
|
|
220
|
+
DEFAULT_MAX_CHECK_ANNOTATIONS
|
|
221
|
+
);
|
|
222
|
+
return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map((testCase) => ({
|
|
223
|
+
path: testCase.displayFile,
|
|
224
|
+
start_line: testCase.location.line,
|
|
225
|
+
end_line: testCase.location.line,
|
|
226
|
+
annotation_level: "failure",
|
|
227
|
+
title: truncate(
|
|
228
|
+
`${testCase.primaryFailure?.judgeName ?? "vitest-evals"} - ${testCase.displayName}`,
|
|
229
|
+
255
|
|
230
|
+
),
|
|
231
|
+
message: truncate(
|
|
232
|
+
formatWorkflowMessage(testCase),
|
|
233
|
+
MAX_CHECK_FIELD_LENGTH
|
|
234
|
+
),
|
|
235
|
+
raw_details: truncate(formatRawDetails(testCase), MAX_CHECK_FIELD_LENGTH)
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
function hasAnnotationLocation(testCase) {
|
|
239
|
+
return Boolean(testCase.location);
|
|
240
|
+
}
|
|
241
|
+
function formatWorkflowMessage(testCase) {
|
|
242
|
+
const failure = testCase.primaryFailure;
|
|
243
|
+
const parts = [
|
|
244
|
+
testCase.displayName,
|
|
245
|
+
`score ${formatScore(failure?.score ?? testCase.eval?.avgScore)}`
|
|
246
|
+
];
|
|
247
|
+
if (failure?.judgeName) {
|
|
248
|
+
parts.push(failure.judgeName);
|
|
249
|
+
}
|
|
250
|
+
const reason = compactLine(failure?.reason ?? "", 320);
|
|
251
|
+
if (reason) {
|
|
252
|
+
parts.push(reason);
|
|
253
|
+
}
|
|
254
|
+
return parts.join(" - ");
|
|
255
|
+
}
|
|
256
|
+
function formatRawDetails(testCase) {
|
|
257
|
+
const lines = [
|
|
258
|
+
`Test: ${testCase.displayName}`,
|
|
259
|
+
`Location: ${testCase.displayFile}:${testCase.location.line}`,
|
|
260
|
+
`Harness: ${testCase.harness?.name ?? "n/a"}`,
|
|
261
|
+
`Score: ${formatScore(testCase.primaryFailure?.score ?? testCase.eval?.avgScore)}`,
|
|
262
|
+
`Judge: ${testCase.primaryFailure?.judgeName ?? "n/a"}`,
|
|
263
|
+
"",
|
|
264
|
+
"Reason:",
|
|
265
|
+
testCase.primaryFailure?.reason ?? "n/a"
|
|
266
|
+
];
|
|
267
|
+
const finalOutput = testCase.eval?.output ?? testCase.harness?.output;
|
|
268
|
+
if (finalOutput !== void 0) {
|
|
269
|
+
lines.push("", "Final:", stringifyValue(finalOutput, 8e3));
|
|
270
|
+
}
|
|
271
|
+
if (testCase.harness?.toolCalls.length) {
|
|
272
|
+
lines.push("", "Tools:");
|
|
273
|
+
for (const toolCall of testCase.harness.toolCalls) {
|
|
274
|
+
lines.push(
|
|
275
|
+
`- ${toolCall.name}: ${toolCall.error ? `error: ${toolCall.error}` : "ok"}`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return lines.join("\n");
|
|
280
|
+
}
|
|
281
|
+
function formatWorkflowCommand({
|
|
282
|
+
command,
|
|
283
|
+
properties,
|
|
284
|
+
message
|
|
285
|
+
}) {
|
|
286
|
+
const renderedProperties = Object.entries(properties).map(([key, value]) => `${key}=${escapeCommandProperty(value)}`).join(",");
|
|
287
|
+
return `::${command} ${renderedProperties}::${escapeCommandData(message)}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
193
290
|
// src/collect.ts
|
|
194
291
|
function collectEvalReport(input, options = {}) {
|
|
195
292
|
const cases = input.testResults.flatMap(
|
|
@@ -310,7 +407,6 @@ function getUsage(value) {
|
|
|
310
407
|
outputTokens: numberField(value.outputTokens),
|
|
311
408
|
reasoningTokens: numberField(value.reasoningTokens),
|
|
312
409
|
totalTokens: numberField(value.totalTokens),
|
|
313
|
-
estimatedCost: numberField(value.estimatedCost),
|
|
314
410
|
toolCalls: numberField(value.toolCalls)
|
|
315
411
|
};
|
|
316
412
|
}
|
|
@@ -380,7 +476,6 @@ function sumUsage(cases) {
|
|
|
380
476
|
outputTokens: 0,
|
|
381
477
|
reasoningTokens: 0,
|
|
382
478
|
totalTokens: 0,
|
|
383
|
-
estimatedCost: 0,
|
|
384
479
|
toolCalls: 0
|
|
385
480
|
};
|
|
386
481
|
for (const testCase of cases) {
|
|
@@ -389,7 +484,6 @@ function sumUsage(cases) {
|
|
|
389
484
|
usage2.outputTokens += caseUsage?.outputTokens ?? 0;
|
|
390
485
|
usage2.reasoningTokens += caseUsage?.reasoningTokens ?? 0;
|
|
391
486
|
usage2.totalTokens += caseUsage?.totalTokens ?? (caseUsage?.inputTokens ?? 0) + (caseUsage?.outputTokens ?? 0) + (caseUsage?.reasoningTokens ?? 0);
|
|
392
|
-
usage2.estimatedCost += caseUsage?.estimatedCost ?? 0;
|
|
393
487
|
usage2.toolCalls += caseUsage?.toolCalls ?? testCase.harness?.toolCalls.length ?? 0;
|
|
394
488
|
}
|
|
395
489
|
return usage2;
|
|
@@ -403,95 +497,6 @@ function resolveRunDuration(input) {
|
|
|
403
497
|
return Math.max(...endTimes) - Math.min(...startTimes);
|
|
404
498
|
}
|
|
405
499
|
|
|
406
|
-
// src/annotations.ts
|
|
407
|
-
var DEFAULT_MAX_WORKFLOW_ANNOTATIONS = 10;
|
|
408
|
-
var DEFAULT_MAX_CHECK_ANNOTATIONS = 50;
|
|
409
|
-
var MAX_CHECK_FIELD_LENGTH = 64e3;
|
|
410
|
-
function renderWorkflowCommands(report, options = {}) {
|
|
411
|
-
const maxAnnotations = options.maxAnnotations ?? DEFAULT_MAX_WORKFLOW_ANNOTATIONS;
|
|
412
|
-
return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map(
|
|
413
|
-
(testCase) => formatWorkflowCommand({
|
|
414
|
-
command: "error",
|
|
415
|
-
properties: {
|
|
416
|
-
file: testCase.displayFile,
|
|
417
|
-
line: String(testCase.location.line),
|
|
418
|
-
col: String(testCase.location.column),
|
|
419
|
-
title: "vitest-evals"
|
|
420
|
-
},
|
|
421
|
-
message: formatWorkflowMessage(testCase)
|
|
422
|
-
})
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
function buildCheckAnnotations(report, options = {}) {
|
|
426
|
-
const maxAnnotations = options.maxAnnotations ?? DEFAULT_MAX_CHECK_ANNOTATIONS;
|
|
427
|
-
return report.failures.filter(hasAnnotationLocation).slice(0, maxAnnotations).map((testCase) => ({
|
|
428
|
-
path: testCase.displayFile,
|
|
429
|
-
start_line: testCase.location.line,
|
|
430
|
-
end_line: testCase.location.line,
|
|
431
|
-
annotation_level: "failure",
|
|
432
|
-
title: truncate(
|
|
433
|
-
`${testCase.primaryFailure?.judgeName ?? "vitest-evals"} - ${testCase.displayName}`,
|
|
434
|
-
255
|
|
435
|
-
),
|
|
436
|
-
message: truncate(
|
|
437
|
-
formatWorkflowMessage(testCase),
|
|
438
|
-
MAX_CHECK_FIELD_LENGTH
|
|
439
|
-
),
|
|
440
|
-
raw_details: truncate(formatRawDetails(testCase), MAX_CHECK_FIELD_LENGTH)
|
|
441
|
-
}));
|
|
442
|
-
}
|
|
443
|
-
function hasAnnotationLocation(testCase) {
|
|
444
|
-
return Boolean(testCase.location);
|
|
445
|
-
}
|
|
446
|
-
function formatWorkflowMessage(testCase) {
|
|
447
|
-
const failure = testCase.primaryFailure;
|
|
448
|
-
const parts = [
|
|
449
|
-
testCase.displayName,
|
|
450
|
-
`score ${formatScore(failure?.score ?? testCase.eval?.avgScore)}`
|
|
451
|
-
];
|
|
452
|
-
if (failure?.judgeName) {
|
|
453
|
-
parts.push(failure.judgeName);
|
|
454
|
-
}
|
|
455
|
-
const reason = compactLine(failure?.reason ?? "", 320);
|
|
456
|
-
if (reason) {
|
|
457
|
-
parts.push(reason);
|
|
458
|
-
}
|
|
459
|
-
return parts.join(" - ");
|
|
460
|
-
}
|
|
461
|
-
function formatRawDetails(testCase) {
|
|
462
|
-
const lines = [
|
|
463
|
-
`Test: ${testCase.displayName}`,
|
|
464
|
-
`Location: ${testCase.displayFile}:${testCase.location.line}`,
|
|
465
|
-
`Harness: ${testCase.harness?.name ?? "n/a"}`,
|
|
466
|
-
`Score: ${formatScore(testCase.primaryFailure?.score ?? testCase.eval?.avgScore)}`,
|
|
467
|
-
`Judge: ${testCase.primaryFailure?.judgeName ?? "n/a"}`,
|
|
468
|
-
"",
|
|
469
|
-
"Reason:",
|
|
470
|
-
testCase.primaryFailure?.reason ?? "n/a"
|
|
471
|
-
];
|
|
472
|
-
const finalOutput = testCase.eval?.output ?? testCase.harness?.output;
|
|
473
|
-
if (finalOutput !== void 0) {
|
|
474
|
-
lines.push("", "Final:", stringifyValue(finalOutput, 8e3));
|
|
475
|
-
}
|
|
476
|
-
if (testCase.harness?.toolCalls.length) {
|
|
477
|
-
lines.push("", "Tools:");
|
|
478
|
-
for (const toolCall of testCase.harness.toolCalls) {
|
|
479
|
-
lines.push(
|
|
480
|
-
`- ${toolCall.name}: ${toolCall.error ? `error: ${toolCall.error}` : "ok"}`
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
return lines.join("\n");
|
|
485
|
-
}
|
|
486
|
-
function formatWorkflowCommand({
|
|
487
|
-
command,
|
|
488
|
-
properties,
|
|
489
|
-
message
|
|
490
|
-
}) {
|
|
491
|
-
const renderedProperties = Object.entries(properties).map(([key, value]) => `${key}=${escapeCommandProperty(value)}`).join(",");
|
|
492
|
-
return `::${command} ${renderedProperties}::${escapeCommandData(message)}`;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
500
|
// src/summary.ts
|
|
496
501
|
var DEFAULT_MAX_FAILURES = 20;
|
|
497
502
|
var DEFAULT_MAX_REASON_CHARS = 8e3;
|
|
@@ -544,14 +549,6 @@ function formatCountLine(passed, failed, total) {
|
|
|
544
549
|
function renderSummaryTable(report, nonEvalFailures) {
|
|
545
550
|
const rows = [
|
|
546
551
|
["Status", report.status],
|
|
547
|
-
[
|
|
548
|
-
"Tests",
|
|
549
|
-
formatCountLine(
|
|
550
|
-
report.totals.passed,
|
|
551
|
-
report.totals.failed,
|
|
552
|
-
report.totals.total
|
|
553
|
-
)
|
|
554
|
-
],
|
|
555
552
|
[
|
|
556
553
|
"Evals",
|
|
557
554
|
formatCountLine(
|
|
@@ -564,10 +561,6 @@ function renderSummaryTable(report, nonEvalFailures) {
|
|
|
564
561
|
if (report.score) {
|
|
565
562
|
rows.push(["Score", formatScoreSummary(report.score)]);
|
|
566
563
|
}
|
|
567
|
-
const usage2 = formatUsage(report.usage);
|
|
568
|
-
if (usage2) {
|
|
569
|
-
rows.push(["Usage", usage2]);
|
|
570
|
-
}
|
|
571
564
|
if (nonEvalFailures > 0) {
|
|
572
565
|
rows.push([
|
|
573
566
|
"Other Failures",
|
|
@@ -606,7 +599,7 @@ function renderScoreDistribution(report) {
|
|
|
606
599
|
}
|
|
607
600
|
const maxCount = Math.max(...counts);
|
|
608
601
|
return [
|
|
609
|
-
"
|
|
602
|
+
"## Scores",
|
|
610
603
|
"",
|
|
611
604
|
"```text",
|
|
612
605
|
...SCORE_DISTRIBUTION_BUCKETS.map(
|
|
@@ -761,21 +754,6 @@ function renderAsciiTable(headers, rows) {
|
|
|
761
754
|
...rows.map(renderRow)
|
|
762
755
|
];
|
|
763
756
|
}
|
|
764
|
-
function formatUsage(usage2) {
|
|
765
|
-
const parts = [];
|
|
766
|
-
if (usage2.totalTokens > 0) {
|
|
767
|
-
parts.push(`${formatNumber(usage2.totalTokens)} tokens`);
|
|
768
|
-
}
|
|
769
|
-
if (usage2.toolCalls > 0) {
|
|
770
|
-
parts.push(
|
|
771
|
-
`${formatNumber(usage2.toolCalls)} tool${usage2.toolCalls === 1 ? "" : "s"}`
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
if (usage2.estimatedCost > 0) {
|
|
775
|
-
parts.push(`$${usage2.estimatedCost.toFixed(4)}`);
|
|
776
|
-
}
|
|
777
|
-
return parts.join(", ");
|
|
778
|
-
}
|
|
779
757
|
function formatCaseUsage(testCase) {
|
|
780
758
|
const usage2 = testCase.harness?.usage;
|
|
781
759
|
const parts = [];
|
|
@@ -880,29 +858,211 @@ function truncateCheckSummary(summary) {
|
|
|
880
858
|
return `${summary.slice(0, MAX_CHECK_SUMMARY_LENGTH - CHECK_SUMMARY_TRUNCATION_SUFFIX.length).trimEnd()}${CHECK_SUMMARY_TRUNCATION_SUFFIX}`;
|
|
881
859
|
}
|
|
882
860
|
|
|
883
|
-
// src/
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
861
|
+
// src/merge.ts
|
|
862
|
+
function mergeEvalReports(reports) {
|
|
863
|
+
const cases = reports.flatMap((report) => report.cases);
|
|
864
|
+
const failures = reports.flatMap((report) => report.failures);
|
|
865
|
+
const scoredCases = cases.map((testCase) => testCase.eval?.avgScore).filter(
|
|
866
|
+
(score) => typeof score === "number" && Number.isFinite(score)
|
|
867
|
+
);
|
|
868
|
+
const startedAtValues = reports.map((report) => report.startedAt).filter(
|
|
869
|
+
(startedAt2) => typeof startedAt2 === "number" && Number.isFinite(startedAt2)
|
|
870
|
+
);
|
|
871
|
+
const startedAt = startedAtValues.length > 0 ? Math.min(...startedAtValues) : void 0;
|
|
872
|
+
return {
|
|
873
|
+
status: reports.some((report) => report.status === "failed") ? "failed" : "passed",
|
|
874
|
+
startedAt,
|
|
875
|
+
durationMs: mergeDuration(reports),
|
|
876
|
+
totals: {
|
|
877
|
+
total: sum(reports, (report) => report.totals.total),
|
|
878
|
+
passed: sum(reports, (report) => report.totals.passed),
|
|
879
|
+
failed: sum(reports, (report) => report.totals.failed),
|
|
880
|
+
skipped: sum(reports, (report) => report.totals.skipped),
|
|
881
|
+
evalTotal: sum(reports, (report) => report.totals.evalTotal),
|
|
882
|
+
evalPassed: sum(reports, (report) => report.totals.evalPassed),
|
|
883
|
+
evalFailed: sum(reports, (report) => report.totals.evalFailed)
|
|
884
|
+
},
|
|
885
|
+
score: scoredCases.length > 0 ? {
|
|
886
|
+
average: scoredCases.reduce((total, score) => total + score, 0) / scoredCases.length,
|
|
887
|
+
minimum: Math.min(...scoredCases)
|
|
888
|
+
} : void 0,
|
|
889
|
+
usage: mergeUsage(reports.map((report) => report.usage)),
|
|
890
|
+
cases,
|
|
891
|
+
failures
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
function mergeUsage(usages) {
|
|
895
|
+
return {
|
|
896
|
+
inputTokens: sum(usages, (usage2) => usage2.inputTokens),
|
|
897
|
+
outputTokens: sum(usages, (usage2) => usage2.outputTokens),
|
|
898
|
+
reasoningTokens: sum(usages, (usage2) => usage2.reasoningTokens),
|
|
899
|
+
totalTokens: sum(usages, (usage2) => usage2.totalTokens),
|
|
900
|
+
toolCalls: sum(usages, (usage2) => usage2.toolCalls)
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function mergeDuration(reports) {
|
|
904
|
+
const intervals = reports.map((report) => {
|
|
905
|
+
if (typeof report.startedAt !== "number" || !Number.isFinite(report.startedAt) || typeof report.durationMs !== "number" || !Number.isFinite(report.durationMs)) {
|
|
906
|
+
return void 0;
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
start: report.startedAt,
|
|
910
|
+
end: report.startedAt + report.durationMs
|
|
911
|
+
};
|
|
912
|
+
}).filter(
|
|
913
|
+
(interval) => Boolean(interval)
|
|
914
|
+
);
|
|
915
|
+
if (intervals.length > 0) {
|
|
916
|
+
return Math.max(...intervals.map((interval) => interval.end)) - Math.min(...intervals.map((interval) => interval.start));
|
|
893
917
|
}
|
|
894
|
-
const
|
|
895
|
-
|
|
918
|
+
const durations = reports.map((report) => report.durationMs).filter(
|
|
919
|
+
(durationMs) => typeof durationMs === "number" && Number.isFinite(durationMs)
|
|
896
920
|
);
|
|
897
|
-
|
|
898
|
-
|
|
921
|
+
return durations.length > 0 ? durations.reduce((total, durationMs) => total + durationMs, 0) : void 0;
|
|
922
|
+
}
|
|
923
|
+
function sum(items, select) {
|
|
924
|
+
return items.reduce((total, item) => total + select(item), 0);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/results.ts
|
|
928
|
+
var import_promises = require("fs/promises");
|
|
929
|
+
var import_node_path2 = require("path");
|
|
930
|
+
var GLOB_META_PATTERN = /[*?]/;
|
|
931
|
+
async function resolveResultFiles(patterns, options = {}) {
|
|
932
|
+
const cwd = options.cwd ?? process.cwd();
|
|
933
|
+
const files = [];
|
|
934
|
+
for (const pattern of patterns.map((entry) => entry.trim()).filter(Boolean)) {
|
|
935
|
+
if (hasGlob(pattern)) {
|
|
936
|
+
files.push(...await expandGlob(pattern, cwd));
|
|
937
|
+
} else {
|
|
938
|
+
files.push((0, import_node_path2.isAbsolute)(pattern) ? pattern : (0, import_node_path2.resolve)(cwd, pattern));
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return [...new Set(files)].sort();
|
|
942
|
+
}
|
|
943
|
+
function hasGlob(pattern) {
|
|
944
|
+
return GLOB_META_PATTERN.test(pattern);
|
|
945
|
+
}
|
|
946
|
+
async function expandGlob(pattern, cwd) {
|
|
947
|
+
const normalizedPattern = normalizeGlobPattern(pattern);
|
|
948
|
+
const absolutePattern = (0, import_node_path2.isAbsolute)(pattern);
|
|
949
|
+
const base = globBase(normalizedPattern);
|
|
950
|
+
const basePath = absolutePattern ? base || import_node_path2.sep : (0, import_node_path2.resolve)(cwd, base || ".");
|
|
951
|
+
const regex = globToRegExp(normalizedPattern);
|
|
952
|
+
const matches = [];
|
|
953
|
+
for (const file of await listFiles(basePath)) {
|
|
954
|
+
const normalizedFile = normalizePath(file);
|
|
955
|
+
const candidate = absolutePattern ? normalizedFile : normalizePath((0, import_node_path2.relative)((0, import_node_path2.resolve)(cwd), file));
|
|
956
|
+
if (regex.test(candidate)) {
|
|
957
|
+
matches.push(file);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return matches;
|
|
961
|
+
}
|
|
962
|
+
function globBase(pattern) {
|
|
963
|
+
const segments = pattern.split("/");
|
|
964
|
+
const baseSegments = [];
|
|
965
|
+
for (const segment of segments) {
|
|
966
|
+
if (hasGlob(segment)) {
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
baseSegments.push(segment);
|
|
970
|
+
}
|
|
971
|
+
return baseSegments.join("/");
|
|
972
|
+
}
|
|
973
|
+
async function listFiles(directory) {
|
|
974
|
+
const entries = await readDirectory(directory);
|
|
975
|
+
if (!entries) {
|
|
976
|
+
return [];
|
|
977
|
+
}
|
|
978
|
+
const files = [];
|
|
979
|
+
for (const entry of entries) {
|
|
980
|
+
const child = (0, import_node_path2.resolve)(directory, entry.name);
|
|
981
|
+
if (entry.isDirectory()) {
|
|
982
|
+
files.push(...await listFiles(child));
|
|
983
|
+
} else if (entry.isFile()) {
|
|
984
|
+
files.push(child);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return files;
|
|
988
|
+
}
|
|
989
|
+
async function readDirectory(directory) {
|
|
990
|
+
try {
|
|
991
|
+
return await (0, import_promises.readdir)(directory, { withFileTypes: true });
|
|
992
|
+
} catch {
|
|
993
|
+
return void 0;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
function globToRegExp(pattern) {
|
|
997
|
+
let source = "";
|
|
998
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
999
|
+
const char = pattern[index];
|
|
1000
|
+
const next = pattern[index + 1];
|
|
1001
|
+
if (char === "*" && next === "*") {
|
|
1002
|
+
const following = pattern[index + 2];
|
|
1003
|
+
if (following === "/") {
|
|
1004
|
+
source += "(?:.*/)?";
|
|
1005
|
+
index += 2;
|
|
1006
|
+
} else {
|
|
1007
|
+
source += ".*";
|
|
1008
|
+
index += 1;
|
|
1009
|
+
}
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
if (char === "*") {
|
|
1013
|
+
source += "[^/]*";
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
if (char === "?") {
|
|
1017
|
+
source += "[^/]";
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
source += escapeRegExp(char ?? "");
|
|
1021
|
+
}
|
|
1022
|
+
return new RegExp(`^${source}$`);
|
|
1023
|
+
}
|
|
1024
|
+
function escapeRegExp(value) {
|
|
1025
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
1026
|
+
}
|
|
1027
|
+
function normalizePath(path) {
|
|
1028
|
+
return path.replace(/\\/g, "/");
|
|
1029
|
+
}
|
|
1030
|
+
function normalizeGlobPattern(pattern) {
|
|
1031
|
+
const normalizedPattern = normalizePath(pattern);
|
|
1032
|
+
if ((0, import_node_path2.isAbsolute)(pattern)) {
|
|
1033
|
+
return normalizedPattern;
|
|
1034
|
+
}
|
|
1035
|
+
return normalizedPattern.replace(/^(\.\/)+/, "");
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// src/report.ts
|
|
1039
|
+
async function publishEvalReport(options) {
|
|
1040
|
+
const resultFiles = await resolveResultFiles(options.resultPatterns, {
|
|
1041
|
+
cwd: options.cwd
|
|
899
1042
|
});
|
|
1043
|
+
if (resultFiles.length === 0) {
|
|
1044
|
+
throw new Error(
|
|
1045
|
+
`No eval result files matched: ${options.resultPatterns.join(", ")}`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
const reports = await Promise.all(
|
|
1049
|
+
resultFiles.map(async (resultFile) => {
|
|
1050
|
+
const json = await readVitestJsonReport(resultFile);
|
|
1051
|
+
return collectEvalReport(json, {
|
|
1052
|
+
workspace: options.workspace
|
|
1053
|
+
});
|
|
1054
|
+
})
|
|
1055
|
+
);
|
|
1056
|
+
const report = mergeEvalReports(reports);
|
|
900
1057
|
const summary = renderJobSummary(report, {
|
|
901
|
-
maxFailures: options.maxFailures
|
|
1058
|
+
maxFailures: options.maxFailures,
|
|
1059
|
+
maxOutputChars: options.maxOutputChars,
|
|
1060
|
+
maxReasonChars: options.maxReasonChars,
|
|
1061
|
+
maxToolCalls: options.maxToolCalls
|
|
902
1062
|
});
|
|
903
|
-
if (options.summaryEnabled) {
|
|
1063
|
+
if (options.summaryEnabled !== false) {
|
|
904
1064
|
if (options.summaryPath) {
|
|
905
|
-
await (0,
|
|
1065
|
+
await (0, import_promises2.appendFile)(options.summaryPath, `${summary}
|
|
906
1066
|
`);
|
|
907
1067
|
} else {
|
|
908
1068
|
console.log(summary);
|
|
@@ -915,28 +1075,81 @@ async function main() {
|
|
|
915
1075
|
console.log(command);
|
|
916
1076
|
}
|
|
917
1077
|
}
|
|
1078
|
+
let checkRun;
|
|
918
1079
|
if (options.checkRun) {
|
|
919
1080
|
try {
|
|
920
|
-
|
|
1081
|
+
checkRun = await publishCheckRun(report, {
|
|
921
1082
|
checkRunId: options.checkRunId,
|
|
922
1083
|
maxAnnotations: options.maxAnnotations,
|
|
923
1084
|
maxFailures: options.maxFailures,
|
|
1085
|
+
maxOutputChars: options.maxOutputChars,
|
|
1086
|
+
maxReasonChars: options.maxReasonChars,
|
|
1087
|
+
maxToolCalls: options.maxToolCalls,
|
|
924
1088
|
name: options.checkName,
|
|
925
1089
|
repository: options.repository,
|
|
926
1090
|
sha: options.sha,
|
|
927
1091
|
token: options.token
|
|
928
1092
|
});
|
|
929
|
-
if (
|
|
930
|
-
warn(`GitHub Check Run skipped: ${
|
|
1093
|
+
if (checkRun.status === "skipped") {
|
|
1094
|
+
options.warn?.(`GitHub Check Run skipped: ${checkRun.reason}`);
|
|
931
1095
|
}
|
|
932
1096
|
} catch (error) {
|
|
933
1097
|
const message = error instanceof Error ? error.message : String(error);
|
|
934
1098
|
if (options.failOnCheckError) {
|
|
935
1099
|
throw error;
|
|
936
1100
|
}
|
|
937
|
-
warn(message);
|
|
1101
|
+
options.warn?.(message);
|
|
938
1102
|
}
|
|
939
1103
|
}
|
|
1104
|
+
return {
|
|
1105
|
+
report,
|
|
1106
|
+
resultFiles,
|
|
1107
|
+
checkRun
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
async function readVitestJsonReport(resultFile) {
|
|
1111
|
+
try {
|
|
1112
|
+
return JSON.parse(await (0, import_promises2.readFile)(resultFile, "utf8"));
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1115
|
+
throw new Error(
|
|
1116
|
+
`Failed to read eval result file ${resultFile}: ${message}`
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/cli.ts
|
|
1122
|
+
main().catch((error) => {
|
|
1123
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1124
|
+
process.exitCode = 1;
|
|
1125
|
+
});
|
|
1126
|
+
async function main() {
|
|
1127
|
+
const options = parseCliArgs(process.argv.slice(2));
|
|
1128
|
+
if (options.help) {
|
|
1129
|
+
console.log(usage());
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const result = await publishEvalReport({
|
|
1133
|
+
resultPatterns: options.resultPatterns,
|
|
1134
|
+
cwd: options.workspace ?? process.env.GITHUB_WORKSPACE ?? process.cwd(),
|
|
1135
|
+
workspace: options.workspace ?? process.env.GITHUB_WORKSPACE ?? process.cwd(),
|
|
1136
|
+
summaryEnabled: options.summaryEnabled,
|
|
1137
|
+
summaryPath: options.summaryPath,
|
|
1138
|
+
annotations: options.annotations,
|
|
1139
|
+
checkRun: options.checkRun,
|
|
1140
|
+
checkRunId: options.checkRunId,
|
|
1141
|
+
checkName: options.checkName,
|
|
1142
|
+
failOnCheckError: options.failOnCheckError,
|
|
1143
|
+
maxAnnotations: options.maxAnnotations,
|
|
1144
|
+
maxFailures: options.maxFailures,
|
|
1145
|
+
repository: options.repository,
|
|
1146
|
+
sha: options.sha,
|
|
1147
|
+
token: options.token,
|
|
1148
|
+
warn
|
|
1149
|
+
});
|
|
1150
|
+
if (options.failOnFailures && result.report.status === "failed") {
|
|
1151
|
+
process.exitCode = 1;
|
|
1152
|
+
}
|
|
940
1153
|
}
|
|
941
1154
|
function warn(message) {
|
|
942
1155
|
if (process.env.GITHUB_ACTIONS === "true") {
|
|
@@ -947,15 +1160,16 @@ function warn(message) {
|
|
|
947
1160
|
}
|
|
948
1161
|
function usage() {
|
|
949
1162
|
return [
|
|
950
|
-
"Usage: vitest-evals-github-report [vitest-results.json] [--json <path>]",
|
|
1163
|
+
"Usage: vitest-evals-github-report [vitest-results.json ...] [--json <path>]",
|
|
951
1164
|
"",
|
|
952
1165
|
"Options:",
|
|
953
|
-
" --json <path> Read Vitest JSON report from this path",
|
|
1166
|
+
" --json <path> Read a Vitest JSON report from this path or glob",
|
|
954
1167
|
" --summary <path> Write job summary markdown to this path",
|
|
955
1168
|
" --no-summary Disable summary output",
|
|
956
1169
|
" --annotations Emit GitHub workflow-command annotations",
|
|
957
1170
|
" --no-annotations Disable workflow-command annotations",
|
|
958
1171
|
" --check-run Publish a GitHub Check Run when configured",
|
|
1172
|
+
" --fail-on-failures Exit non-zero when the combined report failed",
|
|
959
1173
|
" --fail-on-check-error Fail when Check Run publishing fails",
|
|
960
1174
|
" --check-run-id <id> Update an existing Check Run",
|
|
961
1175
|
" --check-name <name> Check Run name (default: vitest-evals)",
|