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