import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { writeFileSync } from "node:fs"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { createPluginSdkApiReleaseEvidence, validatePluginSdkApiReleaseEvidence, } from "../../scripts/plugin-sdk-api-release-evidence.mjs"; import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js"; const baseSha = "a".repeat(40); const headSha = "b".repeat(40); const workflowSha = "d".repeat(40); const tempDirs = useAutoCleanupTempDirTracker(afterEach); function diff(exports: unknown[] = []) { const payload = { entrypointsAdded: [], entrypointsRemoved: [], exports }; return { ...payload, digest: createHash("sha256").update(JSON.stringify(payload), "utf8").digest("hex"), }; } function evidence(exports: unknown[] = []) { return createPluginSdkApiReleaseEvidence({ baseRef: "v2026.8.1", baseSha, diff: diff(exports), headSha, workflowSha, }); } describe("Plugin SDK API release evidence", () => { it("enforces acknowledgement through the release CLI", () => { const receipt = evidence([{ change: "added", exportName: "send" }]); const manifestPath = join(tempDirs.make("plugin-sdk-evidence-"), "manifest.json"); writeFileSync(manifestPath, JSON.stringify({ pluginSdkApi: receipt })); const run = (acknowledgement?: string) => spawnSync( process.execPath, [ "scripts/plugin-sdk-api-release-evidence.mjs", "--manifest", manifestPath, "--head", headSha, "--workflow-sha", workflowSha, ...(acknowledgement ? ["--acknowledge", acknowledgement] : []), ], { cwd: process.cwd(), encoding: "utf8" }, ); expect(run().stderr).toContain("require acknowledgement digest"); expect(run("deadbeef").stderr).toContain("require acknowledgement digest"); const accepted = run(receipt.digest.slice(0, 8)); expect(accepted.status, accepted.stderr).toBe(0); expect(JSON.parse(accepted.stdout)).toMatchObject({ hasChanges: true, status: "checked" }); }); it("rejects blank and mismatched acknowledgements before accepting the reported digest", () => { const receipt = evidence([{ change: "added", exportName: "send" }]); const expected = receipt.digest.slice(0, 8); const validate = (acknowledgement: string) => validatePluginSdkApiReleaseEvidence({ acknowledgement, evidence: receipt, expectedHeadSha: headSha, expectedWorkflowSha: workflowSha, }); expect(() => validate("")).toThrow(`require acknowledgement digest ${expected}`); expect(() => validate("deadbeef")).toThrow(`require acknowledgement digest ${expected}`); expect(validate(expected)).toMatchObject({ acknowledgement: expected, hasChanges: true }); }); it("accepts a blank acknowledgement when the frozen diff has no changes", () => { expect( validatePluginSdkApiReleaseEvidence({ acknowledgement: "", evidence: evidence(), expectedHeadSha: headSha, expectedWorkflowSha: workflowSha, }), ).toMatchObject({ acknowledgement: null, hasChanges: false }); }); it("rejects evidence for another release SHA or a changed diff payload", () => { const receipt = evidence([{ change: "added", exportName: "send" }]); expect(() => validatePluginSdkApiReleaseEvidence({ acknowledgement: receipt.digest.slice(0, 8), evidence: receipt, expectedHeadSha: "c".repeat(40), expectedWorkflowSha: workflowSha, }), ).toThrow("head SHA does not match"); const changed = structuredClone(receipt); changed.diff.exports.push({ change: "removed", exportName: "receive" }); expect(() => validatePluginSdkApiReleaseEvidence({ acknowledgement: receipt.digest.slice(0, 8), evidence: changed, expectedHeadSha: headSha, expectedWorkflowSha: workflowSha, }), ).toThrow("digest does not match"); }); it("rejects a preflight receipt when the npm dist-tag predecessor moved", () => { const receipt = evidence(); const validate = (currentSelectorRef: string, currentSelectorSha: string) => validatePluginSdkApiReleaseEvidence({ acknowledgement: "", currentSelectorRef, currentSelectorSha, evidence: receipt, expectedHeadSha: headSha, expectedWorkflowSha: workflowSha, targetRef: "v2026.8.2", }); expect(validate("v2026.8.1", baseSha)).toMatchObject({ status: "checked" }); expect(() => validate("v2026.8.1-1", "c".repeat(40))).toThrow( "predecessor no longer matches the npm dist-tag", ); expect(validate("v2026.8.2", headSha)).toMatchObject({ status: "checked" }); expect(() => validate("v2026.8.2", "c".repeat(40))).toThrow( "dist-tag target does not match the release SHA", ); }); it("rejects untrusted tooling and unavailable evidence", () => { const receipt = evidence(); expect(() => validatePluginSdkApiReleaseEvidence({ acknowledgement: "", evidence: receipt, expectedHeadSha: headSha, expectedWorkflowSha: undefined, }), ).toThrow("Expected Plugin SDK API evidence workflow SHA"); expect(() => validatePluginSdkApiReleaseEvidence({ acknowledgement: "", evidence: receipt, expectedHeadSha: headSha, expectedWorkflowSha: "f".repeat(40), }), ).toThrow("workflow SHA does not match trusted tooling"); expect(() => validatePluginSdkApiReleaseEvidence({ acknowledgement: "", evidence: { ...receipt, status: "unavailable" }, expectedHeadSha: headSha, expectedWorkflowSha: workflowSha, }), ).toThrow("invalid status"); }); });