mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-21 10:01:37 -06:00
190 lines
7.0 KiB
TypeScript
190 lines
7.0 KiB
TypeScript
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("preserves the frozen v1 payload digest and receipt bytes", () => {
|
|
const legacyDiff = diff([{ change: "added", exportName: "send" }]);
|
|
const receipt = createPluginSdkApiReleaseEvidence({
|
|
baseRef: "v2026.8.1",
|
|
baseSha,
|
|
diff: legacyDiff,
|
|
headSha,
|
|
workflowSha,
|
|
});
|
|
|
|
expect(legacyDiff.digest).toBe(
|
|
"f4b495f34f8c1b72721841242b24d6f8351524c00e34db016af0fd3944f44992",
|
|
);
|
|
expect(JSON.stringify(receipt)).toBe(
|
|
`{"schema":"openclaw.plugin-sdk-api-release-evidence/v1","status":"checked","baseRef":"v2026.8.1","baseSha":"${baseSha}","headSha":"${headSha}","hasChanges":true,"digest":"f4b495f34f8c1b72721841242b24d6f8351524c00e34db016af0fd3944f44992","diff":{"entrypointsAdded":[],"entrypointsRemoved":[],"exports":[{"change":"added","exportName":"send"}],"digest":"f4b495f34f8c1b72721841242b24d6f8351524c00e34db016af0fd3944f44992"},"workflowSha":"${workflowSha}"}`,
|
|
);
|
|
expect(
|
|
validatePluginSdkApiReleaseEvidence({
|
|
acknowledgement: legacyDiff.digest.slice(0, 8),
|
|
evidence: receipt,
|
|
expectedHeadSha: headSha,
|
|
expectedWorkflowSha: workflowSha,
|
|
}),
|
|
).toMatchObject({ acknowledgement: "f4b495f3", hasChanges: true });
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|