diff --git a/packages/react-doctor/tests/ensure-json-report.test.ts b/packages/react-doctor/tests/ensure-json-report.test.ts new file mode 100644 index 0000000000..8abc882387 --- /dev/null +++ b/packages/react-doctor/tests/ensure-json-report.test.ts @@ -0,0 +1,94 @@ +import { spawnSync } from "node:child_process"; +import * as fs from "node:fs"; +import os from "node:os"; +import * as path from "node:path"; +import { afterEach, describe, expect, it } from "vite-plus/test"; + +const REPOSITORY_ROOT = path.resolve(import.meta.dirname, "..", "..", ".."); +const ENSURE_REPORT_SCRIPT_PATH = path.join(REPOSITORY_ROOT, "scripts/ensure-json-report.mjs"); +const tempDirectories: string[] = []; + +const runEnsureJsonReport = (contents?: string) => { + const tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "react-doctor-report-")); + const reportPath = path.join(tempDirectory, "report.json"); + tempDirectories.push(tempDirectory); + + if (contents !== undefined) { + fs.writeFileSync(reportPath, contents); + } + + const result = spawnSync(process.execPath, [ENSURE_REPORT_SCRIPT_PATH, reportPath, "7"], { + encoding: "utf8", + }); + + return { + report: JSON.parse(fs.readFileSync(reportPath, "utf8")), + status: result.status, + }; +}; + +describe("ensure-json-report", () => { + afterEach(() => { + for (const directory of tempDirectories.splice(0)) { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); + + it.each([1, 2, 3])("accepts schema version %s reports", (schemaVersion) => { + const contents = JSON.stringify({ schemaVersion, ok: true }); + const { report, status } = runEnsureJsonReport(contents); + + expect(status).toBe(0); + expect(report).toEqual({ schemaVersion, ok: true }); + }); + + it.each([ + { + contents: "", + message: "react-doctor exited with status 7 but produced an empty report.", + }, + { + contents: "{", + message: "react-doctor produced output that is not valid JSON.", + }, + { + contents: "null", + message: "react-doctor produced JSON that is not a report object.", + }, + { + contents: "{}", + message: + "react-doctor produced a JSON report without a schemaVersion. The installed CLI may be incompatible with this GitHub Action version.", + }, + { + contents: '{"schemaVersion":999,"ok":true}', + message: + "react-doctor produced schema version 999, but this GitHub Action supports 1, 2, 3. Update millionco/react-doctor@v2 or pin the CLI version to match the Action release.", + }, + { + contents: '{"schemaVersion":3}', + message: "react-doctor produced a schema version 3 report without the required ok field.", + }, + ])("writes a valid fallback report for malformed output", ({ contents, message }) => { + const { report, status } = runEnsureJsonReport(contents); + + expect(status).toBe(1); + expect(report).toMatchObject({ + schemaVersion: 3, + ok: false, + error: { + name: "ReactDoctorActionError", + message, + }, + }); + }); + + it("reports when the CLI did not create a readable report", () => { + const { report, status } = runEnsureJsonReport(); + + expect(status).toBe(1); + expect(report.error.message).toBe( + "react-doctor exited with status 7 without producing a readable JSON report.", + ); + }); +}); diff --git a/scripts/ensure-json-report.mjs b/scripts/ensure-json-report.mjs index 27948cb86d..da351508c7 100644 --- a/scripts/ensure-json-report.mjs +++ b/scripts/ensure-json-report.mjs @@ -7,7 +7,7 @@ if (!reportPath) { process.exit(0); } -const fallbackReport = { +const buildFallbackReport = (errorMessage) => ({ schemaVersion: 3, version: "unknown", ok: false, @@ -27,24 +27,62 @@ const fallbackReport = { elapsedMilliseconds: 0, error: { name: "ReactDoctorActionError", - message: `react-doctor exited with status ${Number.isFinite(status) ? status : 1} before producing a JSON report.`, + message: errorMessage, chain: [], }, +}); + +const failWithReport = (errorMessage) => { + fs.writeFileSync(reportPath, `${JSON.stringify(buildFallbackReport(errorMessage))}\n`); + process.exit(1); }; -// Known JsonReport schema versions remain valid CLI output; only an -// unparseable or unrecognized payload is treated as a failed scan. const KNOWN_SCHEMA_VERSIONS = new Set([1, 2, 3]); +let raw; + +try { + raw = fs.readFileSync(reportPath, "utf8").trim(); +} catch { + failWithReport( + `react-doctor exited with status ${Number.isFinite(status) ? status : 1} without producing a readable JSON report.`, + ); +} + +if (!raw) { + failWithReport( + `react-doctor exited with status ${Number.isFinite(status) ? status : 1} but produced an empty report.`, + ); +} + +let parsed; + try { - const raw = fs.readFileSync(reportPath, "utf8").trim(); - const parsed = JSON.parse(raw); - if (parsed && KNOWN_SCHEMA_VERSIONS.has(parsed.schemaVersion) && typeof parsed.ok === "boolean") { - process.exit(0); - } + parsed = JSON.parse(raw); } catch { - // Fall through to the fallback report. + failWithReport("react-doctor produced output that is not valid JSON."); +} + +if (!parsed || typeof parsed !== "object") { + failWithReport("react-doctor produced JSON that is not a report object."); +} + +if (typeof parsed.schemaVersion !== "number") { + failWithReport( + "react-doctor produced a JSON report without a schemaVersion. The installed CLI may be incompatible with this GitHub Action version.", + ); +} + +if (!KNOWN_SCHEMA_VERSIONS.has(parsed.schemaVersion)) { + failWithReport( + `react-doctor produced schema version ${parsed.schemaVersion}, but this GitHub Action supports ${Array.from(KNOWN_SCHEMA_VERSIONS).sort().join(", ")}. Update millionco/react-doctor@v2 or pin the CLI version to match the Action release.`, + ); +} + +if (typeof parsed.ok !== "boolean") { + failWithReport( + `react-doctor produced a schema version ${parsed.schemaVersion} report without the required ok field.`, + ); } -fs.writeFileSync(reportPath, `${JSON.stringify(fallbackReport)}\n`); -process.exit(1); +process.exit(0);