mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-15 23:24:03 -06:00
d2afbd05ad
* refactor(plugin-sdk): replace API baselines with diffs * perf(plugin-sdk): bound API diff resources * fix(plugin-sdk): isolate API diff dependencies * fix(release): forward Plugin SDK acknowledgement * fix(release): enforce SDK acknowledgement on publish * chore: preserve generated-doc ignore policy * fix(release): freeze SDK API evidence before publish * fix(ci): satisfy SDK evidence guards * fix(release): bind complete SDK evidence * fix(release): authenticate plugin SDK evidence * fix(plugin-sdk): abort interrupted API diffs * test(ui): freeze page clock in background-tasks rail e2e The rail transcript is compared byte-for-byte across the detail-panel round-trip while it renders live relative ages; on slow CI runners the second boundary ticks between the two reads (11s -> 12s) and fails the equality assertion. Fix the page Date with Playwright setFixedTime while keeping timers running so the tasks.list polling assertions still hold. Repro: a 1.5s stall between the reads fails pre-fix with the exact CI diff and passes post-fix. * fix(scripts): drop unused export on dependency-evidence CLI main Knip's workflow scan re-roots script references after an actions/checkout step that sets path:, so the new trusted-tooling checkout in openclaw-npm-release.yml stops marking this CLI as a workflow entry and its exported main() surfaces as an unused export in check-dependencies. Nothing imports main; the module invokes it through its own entry guard, so the export keyword was dead surface either way. --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
164 lines
5.8 KiB
TypeScript
164 lines
5.8 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("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");
|
|
});
|
|
});
|