diff --git a/scripts/test-report-utils.mjs b/scripts/test-report-utils.mjs index 7318f9cad14..0527d20aa14 100644 --- a/scripts/test-report-utils.mjs +++ b/scripts/test-report-utils.mjs @@ -1,5 +1,6 @@ // 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"; @@ -59,6 +60,10 @@ function validateVitestJsonReport(reportPath) { return null; } +function defaultVitestJsonReportPath(prefix) { + return path.join(os.tmpdir(), `${prefix}-${process.pid}-${Date.now()}-${randomUUID()}.json`); +} + /** * Runs Vitest with the JSON reporter unless an existing report was supplied. */ @@ -67,7 +72,7 @@ export function runVitestJsonReport({ reportPath = "", prefix = "openclaw-vitest-report", }) { - const resolvedReportPath = reportPath || path.join(os.tmpdir(), `${prefix}-${Date.now()}.json`); + const resolvedReportPath = reportPath || defaultVitestJsonReportPath(prefix); if (!(reportPath && fs.existsSync(resolvedReportPath))) { const run = spawnSync( diff --git a/test/scripts/test-report-utils.test.ts b/test/scripts/test-report-utils.test.ts index fa73da36231..f051c55dc40 100644 --- a/test/scripts/test-report-utils.test.ts +++ b/test/scripts/test-report-utils.test.ts @@ -153,6 +153,42 @@ describe("scripts/test-report-utils runVitestJsonReport", () => { ); }); + it("uses distinct default report paths when invocations share a clock tick", async () => { + const { runVitestJsonReport } = await import("../../scripts/test-report-utils.mjs"); + const reportPaths: string[] = []; + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1234567890); + spawnSyncMock.mockImplementation((_command: string, args: string[]) => { + const outputFileIndex = args.indexOf("--outputFile") + 1; + const outputFile = args[outputFileIndex]; + reportPaths.push(outputFile); + fs.writeFileSync(outputFile, `${JSON.stringify({ testResults: [] })}\n`, "utf8"); + return { status: 0 }; + }); + + try { + runVitestJsonReport({ + config: "test/vitest/vitest.unit.config.ts", + }); + runVitestJsonReport({ + config: "test/vitest/vitest.unit.config.ts", + }); + + expect(reportPaths).toHaveLength(2); + expect(reportPaths[0]).not.toBe(reportPaths[1]); + for (const reportPath of reportPaths) { + expect(path.dirname(reportPath)).toBe(os.tmpdir()); + expect(path.basename(reportPath)).toMatch( + /^openclaw-vitest-report-\d+-1234567890-[0-9a-f-]+\.json$/u, + ); + } + } finally { + nowSpy.mockRestore(); + for (const reportPath of reportPaths) { + fs.rmSync(reportPath, { force: true }); + } + } + }); + it("fails when Vitest exits successfully without writing a JSON report", async () => { const { runVitestJsonReport } = await import("../../scripts/test-report-utils.mjs"); spawnSyncMock.mockReturnValue({ status: 0 });