@vitest-evals/github-reporter 0.11.0 → 0.13.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/dist/cli.mjs CHANGED
@@ -101,13 +101,14 @@ function readInteger(args, index, flag) {
101
101
  }
102
102
 
103
103
  // src/report.ts
104
- import { appendFile, readFile } from "fs/promises";
104
+ import { appendFile } from "fs/promises";
105
+ import {
106
+ readVitestJsonReportFile,
107
+ resolveResultFiles
108
+ } from "@vitest-evals/core/node";
105
109
 
106
110
  // src/utils.ts
107
111
  import { posix } from "path";
108
- function isRecord(value) {
109
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
110
- }
111
112
  function compactLine(value, maxLength) {
112
113
  const line = value.split(/\r?\n/).map((part) => part.trim()).find((part) => part.length > 0);
113
114
  if (!line) {
@@ -170,17 +171,6 @@ function formatLocation(file, location) {
170
171
  }
171
172
  return `${file}:${location.line}`;
172
173
  }
173
- function normalizePathForGitHub(path, workspace) {
174
- const normalized = path.replace(/\\/g, "/");
175
- if (!workspace) {
176
- return normalized;
177
- }
178
- const workspacePath = workspace.replace(/\\/g, "/").replace(/\/+$/, "");
179
- if (normalized !== workspacePath && !normalized.startsWith(`${workspacePath}/`)) {
180
- return normalized;
181
- }
182
- return posix.relative(workspacePath, normalized);
183
- }
184
174
  function escapeFence(value) {
185
175
  return value.replace(/```/g, "'''");
186
176
  }
@@ -267,9 +257,9 @@ function formatRawDetails(testCase) {
267
257
  if (finalOutput !== void 0) {
268
258
  lines.push("", "Final:", stringifyValue(finalOutput, 8e3));
269
259
  }
270
- if (testCase.harness?.toolCalls.length) {
260
+ if (testCase.toolCalls.length) {
271
261
  lines.push("", "Tools:");
272
- for (const toolCall of testCase.harness.toolCalls) {
262
+ for (const toolCall of testCase.toolCalls) {
273
263
  lines.push(
274
264
  `- ${toolCall.name}: ${toolCall.error ? `error: ${toolCall.error}` : "ok"}`
275
265
  );
@@ -287,17 +277,23 @@ function formatWorkflowCommand({
287
277
  }
288
278
 
289
279
  // src/collect.ts
280
+ import {
281
+ collectReportWorkspace
282
+ } from "@vitest-evals/core";
290
283
  function collectEvalReport(input, options = {}) {
291
- const cases = input.testResults.flatMap(
292
- (file) => file.assertionResults.flatMap((assertion) => {
293
- const evalCase = collectEvalCase(file, assertion, options);
294
- return evalCase ? [evalCase] : [];
295
- })
284
+ const workspace = collectReportWorkspace(
285
+ {
286
+ report: input
287
+ },
288
+ {
289
+ workspace: options.workspace
290
+ }
296
291
  );
292
+ const cases = workspace.cases.map(collectEvalCase);
297
293
  const failures = cases.filter((testCase) => testCase.status === "failed");
298
294
  const evalScores = cases.map((testCase) => testCase.eval?.avgScore).filter((score) => isFiniteNumber(score));
299
295
  const usage2 = sumUsage(cases);
300
- const durationMs = resolveRunDuration(input);
296
+ const durationMs = workspace.runs[0]?.durationMs;
301
297
  return {
302
298
  status: input.success && failures.length === 0 ? "passed" : "failed",
303
299
  startedAt: input.startTime,
@@ -320,35 +316,29 @@ function collectEvalReport(input, options = {}) {
320
316
  failures
321
317
  };
322
318
  }
323
- function collectEvalCase(file, assertion, options) {
324
- const meta = isRecord(assertion.meta) ? assertion.meta : {};
325
- const evalMeta = getEvalMeta(meta.eval);
326
- const harnessMeta = getHarnessMeta(meta.harness);
327
- if (!evalMeta && !harnessMeta) {
328
- return null;
329
- }
330
- const displayFile = normalizePathForGitHub(file.name, options.workspace);
331
- const scores = evalMeta?.scores ?? [];
332
- const harnessRun = harnessMeta?.run;
333
- const toolCalls = collectToolCalls(harnessRun?.session);
319
+ function collectEvalCase(reportCase) {
320
+ const scores = (reportCase.eval?.scores ?? []).map(normalizeScore);
321
+ const harnessRun = reportCase.harness?.run;
322
+ const toolCalls = collectToolCalls(reportCase);
334
323
  const evalCase = {
335
- id: `${file.name}:${assertion.location?.line ?? 0}:${assertion.fullName}`,
336
- file: file.name,
337
- displayFile,
338
- title: assertion.title,
339
- displayName: formatDisplayName(assertion),
340
- status: assertion.status,
341
- durationMs: typeof assertion.duration === "number" ? assertion.duration : void 0,
342
- location: assertion.location ?? void 0,
343
- failureMessages: assertion.failureMessages ?? [],
344
- eval: evalMeta ? {
345
- avgScore: evalMeta.avgScore,
346
- thresholdFailed: evalMeta.thresholdFailed,
347
- output: evalMeta.output,
324
+ id: reportCase.id,
325
+ file: reportCase.file,
326
+ displayFile: reportCase.displayFile,
327
+ title: reportCase.title,
328
+ displayName: reportCase.displayName,
329
+ status: reportCase.status,
330
+ durationMs: numberField(reportCase.durationMs),
331
+ location: reportCase.location,
332
+ failureMessages: reportCase.failureMessages,
333
+ toolCalls,
334
+ eval: reportCase.eval ? {
335
+ avgScore: numberField(reportCase.eval.avgScore),
336
+ thresholdFailed: reportCase.eval.thresholdFailed,
337
+ output: reportCase.eval.output,
348
338
  scores
349
339
  } : void 0,
350
- harness: harnessMeta ? {
351
- name: harnessMeta.name,
340
+ harness: reportCase.harness ? {
341
+ name: reportCase.harness.name,
352
342
  output: harnessRun?.output,
353
343
  usage: harnessRun?.usage,
354
344
  timingMs: harnessRun?.timings?.totalMs,
@@ -359,54 +349,10 @@ function collectEvalCase(file, assertion, options) {
359
349
  evalCase.primaryFailure = getPrimaryFailure(evalCase);
360
350
  return evalCase;
361
351
  }
362
- function getEvalMeta(value) {
363
- if (!isRecord(value)) {
364
- return void 0;
365
- }
366
- return {
367
- scores: Array.isArray(value.scores) ? value.scores.filter(isRecord).map(normalizeScore) : void 0,
368
- avgScore: numberField(value.avgScore),
369
- output: value.output,
370
- thresholdFailed: typeof value.thresholdFailed === "boolean" ? value.thresholdFailed : void 0
371
- };
372
- }
373
352
  function normalizeScore(score) {
374
- const metadata = isRecord(score.metadata) ? score.metadata : void 0;
375
- return {
376
- name: typeof score.name === "string" ? score.name : void 0,
377
- score: numberField(score.score) ?? null,
378
- metadata
379
- };
380
- }
381
- function getHarnessMeta(value) {
382
- if (!isRecord(value)) {
383
- return void 0;
384
- }
385
- const run = isRecord(value.run) ? value.run : void 0;
386
- const usage2 = isRecord(run?.usage) ? getUsage(run.usage) : void 0;
387
- const timings = isRecord(run?.timings) ? run.timings : void 0;
388
353
  return {
389
- name: typeof value.name === "string" ? value.name : void 0,
390
- run: run ? {
391
- output: run.output,
392
- usage: usage2,
393
- timings: {
394
- totalMs: typeof timings?.totalMs === "number" ? timings.totalMs : void 0
395
- },
396
- session: isRecord(run.session) ? {
397
- messages: Array.isArray(run.session.messages) ? run.session.messages : void 0
398
- } : void 0,
399
- errors: Array.isArray(run.errors) ? run.errors : void 0
400
- } : void 0
401
- };
402
- }
403
- function getUsage(value) {
404
- return {
405
- inputTokens: numberField(value.inputTokens),
406
- outputTokens: numberField(value.outputTokens),
407
- reasoningTokens: numberField(value.reasoningTokens),
408
- totalTokens: numberField(value.totalTokens),
409
- toolCalls: numberField(value.toolCalls)
354
+ ...score,
355
+ score: numberField(score.score) ?? null
410
356
  };
411
357
  }
412
358
  function numberField(value) {
@@ -415,7 +361,13 @@ function numberField(value) {
415
361
  function isFiniteNumber(value) {
416
362
  return typeof value === "number" && Number.isFinite(value);
417
363
  }
418
- function collectToolCalls(session) {
364
+ function collectToolCalls(reportCase) {
365
+ const harnessCalls = collectSessionToolCalls(
366
+ reportCase.harness?.run?.session
367
+ );
368
+ return harnessCalls.length > 0 ? harnessCalls : (reportCase.eval?.toolCalls ?? []).map(toToolCallSummary);
369
+ }
370
+ function collectSessionToolCalls(session) {
419
371
  const messages = session?.messages ?? [];
420
372
  const toolCalls = [];
421
373
  for (const message of messages) {
@@ -423,21 +375,22 @@ function collectToolCalls(session) {
423
375
  continue;
424
376
  }
425
377
  for (const call of message.toolCalls) {
426
- if (!isRecord(call) || typeof call.name !== "string") {
427
- continue;
428
- }
429
- toolCalls.push({
430
- name: call.name,
431
- error: getToolCallError(call.error),
432
- durationMs: numberField(call.durationMs)
433
- });
378
+ toolCalls.push(toToolCallSummary(call));
434
379
  }
435
380
  }
436
381
  return toolCalls;
437
382
  }
383
+ function toToolCallSummary(call) {
384
+ return {
385
+ name: call.name,
386
+ error: getToolCallError(call.error),
387
+ durationMs: numberField(call.durationMs)
388
+ };
389
+ }
438
390
  function getToolCallError(value) {
439
- if (isRecord(value) && typeof value.message === "string") {
440
- return value.message;
391
+ const error = value;
392
+ if (value && typeof value === "object" && !Array.isArray(value) && typeof error.message === "string") {
393
+ return error.message;
441
394
  }
442
395
  if (value !== void 0) {
443
396
  return stringifyValue(value, 240);
@@ -466,9 +419,6 @@ function stringifyReason(value) {
466
419
  }
467
420
  return typeof value === "string" ? value : stringifyValue(value, 4e3);
468
421
  }
469
- function formatDisplayName(assertion) {
470
- return [...assertion.ancestorTitles, assertion.title].filter((part) => part.length > 0).join(" > ");
471
- }
472
422
  function sumUsage(cases) {
473
423
  const usage2 = {
474
424
  inputTokens: 0,
@@ -483,17 +433,16 @@ function sumUsage(cases) {
483
433
  usage2.outputTokens += caseUsage?.outputTokens ?? 0;
484
434
  usage2.reasoningTokens += caseUsage?.reasoningTokens ?? 0;
485
435
  usage2.totalTokens += caseUsage?.totalTokens ?? (caseUsage?.inputTokens ?? 0) + (caseUsage?.outputTokens ?? 0) + (caseUsage?.reasoningTokens ?? 0);
486
- usage2.toolCalls += caseUsage?.toolCalls ?? testCase.harness?.toolCalls.length ?? 0;
436
+ usage2.toolCalls += toolCallCount(testCase);
487
437
  }
488
438
  return usage2;
489
439
  }
490
- function resolveRunDuration(input) {
491
- const startTimes = input.testResults.map((file) => file.startTime).filter((time) => Number.isFinite(time));
492
- const endTimes = input.testResults.map((file) => file.endTime).filter((time) => Number.isFinite(time));
493
- if (startTimes.length === 0 || endTimes.length === 0) {
494
- return void 0;
440
+ function toolCallCount(testCase) {
441
+ const usageToolCalls = testCase.harness?.usage?.toolCalls;
442
+ if (usageToolCalls !== void 0) {
443
+ return Math.max(usageToolCalls, testCase.toolCalls.length);
495
444
  }
496
- return Math.max(...endTimes) - Math.min(...startTimes);
445
+ return testCase.toolCalls.length;
497
446
  }
498
447
 
499
448
  // src/summary.ts
@@ -700,8 +649,8 @@ function renderFailureBlock(testCase, {
700
649
  ""
701
650
  );
702
651
  }
703
- if (testCase.harness?.toolCalls.length) {
704
- const toolCalls = testCase.harness.toolCalls.slice(0, maxToolCalls);
652
+ if (testCase.toolCalls.length) {
653
+ const toolCalls = testCase.toolCalls.slice(0, maxToolCalls);
705
654
  lines.push(
706
655
  ...renderAsciiTable(
707
656
  ["Tool", "Status", "Duration"],
@@ -712,9 +661,9 @@ function renderFailureBlock(testCase, {
712
661
  ])
713
662
  )
714
663
  );
715
- if (testCase.harness.toolCalls.length > maxToolCalls) {
664
+ if (testCase.toolCalls.length > maxToolCalls) {
716
665
  lines.push(
717
- `${testCase.harness.toolCalls.length - maxToolCalls} more tool calls omitted`
666
+ `${testCase.toolCalls.length - maxToolCalls} more tool calls omitted`
718
667
  );
719
668
  }
720
669
  lines.push("");
@@ -757,7 +706,7 @@ function formatCaseUsage(testCase) {
757
706
  const usage2 = testCase.harness?.usage;
758
707
  const parts = [];
759
708
  const totalTokens = usage2?.totalTokens ?? (usage2?.inputTokens ?? 0) + (usage2?.outputTokens ?? 0) + (usage2?.reasoningTokens ?? 0);
760
- const toolCalls = usage2?.toolCalls ?? testCase.harness?.toolCalls.length ?? 0;
709
+ const toolCalls = usage2?.toolCalls !== void 0 ? Math.max(usage2.toolCalls, testCase.toolCalls.length) : testCase.toolCalls.length;
761
710
  if (totalTokens > 0) {
762
711
  parts.push(`${formatNumber(totalTokens)} tokens`);
763
712
  }
@@ -900,6 +849,9 @@ function mergeUsage(usages) {
900
849
  };
901
850
  }
902
851
  function mergeDuration(reports) {
852
+ const durations = reports.map((report) => report.durationMs).filter(
853
+ (durationMs) => typeof durationMs === "number" && Number.isFinite(durationMs)
854
+ );
903
855
  const intervals = reports.map((report) => {
904
856
  if (typeof report.startedAt !== "number" || !Number.isFinite(report.startedAt) || typeof report.durationMs !== "number" || !Number.isFinite(report.durationMs)) {
905
857
  return void 0;
@@ -911,129 +863,15 @@ function mergeDuration(reports) {
911
863
  }).filter(
912
864
  (interval) => Boolean(interval)
913
865
  );
914
- if (intervals.length > 0) {
866
+ if (intervals.length > 0 && intervals.length === durations.length) {
915
867
  return Math.max(...intervals.map((interval) => interval.end)) - Math.min(...intervals.map((interval) => interval.start));
916
868
  }
917
- const durations = reports.map((report) => report.durationMs).filter(
918
- (durationMs) => typeof durationMs === "number" && Number.isFinite(durationMs)
919
- );
920
869
  return durations.length > 0 ? durations.reduce((total, durationMs) => total + durationMs, 0) : void 0;
921
870
  }
922
871
  function sum(items, select) {
923
872
  return items.reduce((total, item) => total + select(item), 0);
924
873
  }
925
874
 
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
875
  // src/report.ts
1038
876
  async function publishEvalReport(options) {
1039
877
  const resultFiles = await resolveResultFiles(options.resultPatterns, {
@@ -1046,7 +884,7 @@ async function publishEvalReport(options) {
1046
884
  }
1047
885
  const reports = await Promise.all(
1048
886
  resultFiles.map(async (resultFile) => {
1049
- const json = await readVitestJsonReport(resultFile);
887
+ const json = await readVitestJsonReportFile(resultFile);
1050
888
  return collectEvalReport(json, {
1051
889
  workspace: options.workspace
1052
890
  });
@@ -1106,16 +944,6 @@ async function publishEvalReport(options) {
1106
944
  checkRun
1107
945
  };
1108
946
  }
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
947
 
1120
948
  // src/cli.ts
1121
949
  main().catch((error) => {