Files
openclaw/scripts/test-report-utils.mts
Peter Steinberger c70aee247e refactor(scripts): migrate JavaScript tools to TypeScript (#121005)
* refactor(scripts): migrate JavaScript tools to TypeScript

* fix(ci): keep changed-scope preflight zero-install

* fix(ci): preserve zero-install script owners

* fix(ci): complete script migration follow-through

* fix(release): keep stable closeout zero-install

* fix(scripts): preserve standalone execution boundaries

* fix(scripts): repair standalone loader boundaries

* fix(scripts): normalize gateway observation ids

* fix(scripts): keep Docker packager standalone

* test(scripts): preserve rebase cleanup helpers

* test(sessions): use tracked temp directory
2026-08-09 07:21:35 -07:00

172 lines
5.1 KiB
TypeScript

// Shared helpers for running Vitest JSON reports and reading duration data.
import { spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
const normalizeRepoPath = (value: string) => value.split(path.sep).join("/");
const identity = (value: string) => value;
const repoRoot = path.resolve(process.cwd());
/**
* Normalizes absolute or relative file names to repo-relative POSIX paths.
*/
export function normalizeTrackedRepoPath(value: unknown) {
const normalizedValue = typeof value === "string" ? value : "";
const repoRelative = path.isAbsolute(normalizedValue)
? path.relative(repoRoot, path.resolve(normalizedValue))
: normalizedValue;
if (path.isAbsolute(repoRelative) || repoRelative.startsWith("..") || repoRelative === "") {
return normalizeRepoPath(normalizedValue);
}
return normalizeRepoPath(repoRelative);
}
/**
* Reads and parses a JSON file.
*/
export function readJsonFile(filePath: fs.PathOrFileDescriptor) {
const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf8"));
return parsed;
}
/**
* Reads a JSON file or returns the provided fallback on failure.
*/
export function tryReadJsonFile(filePath: fs.PathOrFileDescriptor, fallback: unknown) {
try {
return readJsonFile(filePath);
} catch {
return fallback;
}
}
function validateVitestJsonReport(reportPath: fs.PathLike) {
const displayPath = typeof reportPath === "string" ? reportPath : reportPath.toString();
if (!fs.existsSync(reportPath)) {
return `missing Vitest JSON report: ${displayPath}`;
}
try {
const report = readJsonFile(reportPath);
if (!isRecord(report)) {
return `invalid Vitest JSON report: ${displayPath} (report must be an object)`;
}
if (!Array.isArray(report.testResults)) {
return `invalid Vitest JSON report: ${displayPath} (missing testResults array)`;
}
} catch (error) {
return `invalid Vitest JSON report: ${displayPath} (${
error instanceof Error ? error.message : String(error)
})`;
}
return null;
}
function defaultVitestJsonReportPath(prefix: string) {
return path.join(os.tmpdir(), `${prefix}-${process.pid}-${Date.now()}-${randomUUID()}.json`);
}
/**
* Runs Vitest with the JSON reporter unless an existing report was supplied.
*/
export function runVitestJsonReport({
config,
reportPath = "",
prefix = "openclaw-vitest-report",
}: {
config: string;
reportPath?: string;
prefix?: string;
}) {
const resolvedReportPath = reportPath || defaultVitestJsonReportPath(prefix);
if (!(reportPath && fs.existsSync(resolvedReportPath))) {
const run = spawnSync(
"pnpm",
[
"exec",
"vitest",
"run",
"--config",
config,
"--reporter=json",
"--outputFile",
resolvedReportPath,
],
{
stdio: "inherit",
env: process.env,
},
);
if (run.status !== 0) {
process.exit(run.status ?? 1);
}
}
const invalidReport = validateVitestJsonReport(resolvedReportPath);
if (invalidReport) {
console.error(`[test-report-utils] ${invalidReport}`);
process.exit(1);
}
return resolvedReportPath;
}
/**
* Extracts per-file durations from a Vitest JSON report.
*/
export function collectVitestFileDurations(report: unknown, normalizeFile = identity) {
return readTestResults(report)
.map((result) => {
const file = typeof result.name === "string" ? normalizeFile(result.name) : "";
const start = typeof result.startTime === "number" ? result.startTime : 0;
const end = typeof result.endTime === "number" ? result.endTime : 0;
const testCount = Array.isArray(result.assertionResults) ? result.assertionResults.length : 0;
return {
file,
durationMs: Math.max(0, end - start),
testCount,
};
})
.filter((entry) => entry.file.length > 0 && entry.durationMs > 0);
}
/**
* Extracts per-assertion durations from a Vitest JSON report.
*/
export function collectVitestAssertionDurations(report: unknown, normalizeFile = identity) {
return readTestResults(report).flatMap((result) => {
const file = typeof result.name === "string" ? normalizeFile(result.name) : "";
if (!file) {
return [];
}
const assertions = Array.isArray(result.assertionResults)
? result.assertionResults.filter(isRecord)
: [];
return assertions
.map((assertion) => {
const durationMs =
typeof assertion?.duration === "number" && Number.isFinite(assertion.duration)
? assertion.duration
: 0;
return {
file,
durationMs,
fullName: typeof assertion?.fullName === "string" ? assertion.fullName : "",
status: typeof assertion?.status === "string" ? assertion.status : "unknown",
};
})
.filter((entry) => entry.durationMs > 0);
});
}
function readTestResults(report: unknown) {
if (!isRecord(report) || !Array.isArray(report.testResults)) {
return [];
}
return report.testResults.filter(isRecord);
}