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