Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
868
test/openclaw-npm-postpublish-verify.test.ts
Normal file
868
test/openclaw-npm-postpublish-verify.test.ts
Normal file
@@ -0,0 +1,868 @@
|
||||
import { generateKeyPairSync, sign } from "node:crypto";
|
||||
// OpenClaw npm postpublish tests validate postpublish verification behavior.
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
buildPublishedInstallCommandArgs,
|
||||
buildPublishedInstallScenarios,
|
||||
collectInstalledBundledExtensionManifestErrors,
|
||||
collectInstalledBundledRuntimeSidecarPaths,
|
||||
collectInstalledContextEngineRuntimeErrors,
|
||||
collectInstalledPluginSdkZodArtifactErrors,
|
||||
collectInstalledRootDependencyManifestErrors,
|
||||
collectInstalledPackageErrors,
|
||||
fetchRegistryJson,
|
||||
normalizeInstalledBinaryVersion,
|
||||
openClawNpmPostpublishVerifyUsage,
|
||||
parseOpenClawNpmPostpublishVerifyArgs,
|
||||
resolveInstalledBinaryCommandInvocation,
|
||||
resolveInstalledBinaryPath,
|
||||
retryNpmRegistryProvenanceRead,
|
||||
verifyNpmProvenanceAttestation,
|
||||
verifyNpmRegistrySignatures,
|
||||
} from "../scripts/openclaw-npm-postpublish-verify.ts";
|
||||
|
||||
const INSTALLED_ROOT_DIST_JS_FILE_SCAN_LIMIT = 10_000;
|
||||
|
||||
describe("parseOpenClawNpmPostpublishVerifyArgs", () => {
|
||||
it("supports help and package-manager separators", () => {
|
||||
expect(parseOpenClawNpmPostpublishVerifyArgs(["--help"])).toEqual({
|
||||
help: true,
|
||||
version: "",
|
||||
});
|
||||
expect(parseOpenClawNpmPostpublishVerifyArgs(["--", "2026.3.23"])).toEqual({
|
||||
help: false,
|
||||
version: "2026.3.23",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects missing, option-like, and extra arguments before verification", () => {
|
||||
expect(() => parseOpenClawNpmPostpublishVerifyArgs([])).toThrow(
|
||||
openClawNpmPostpublishVerifyUsage(),
|
||||
);
|
||||
expect(() => parseOpenClawNpmPostpublishVerifyArgs(["--tag"])).toThrow(
|
||||
"Unknown openclaw npm postpublish verifier option: --tag",
|
||||
);
|
||||
expect(() => parseOpenClawNpmPostpublishVerifyArgs(["2026.3.23", "extra"])).toThrow(
|
||||
"Unexpected openclaw npm postpublish verifier argument: extra",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function writeDistJavaScriptFiles(packageRoot: string, count: number): void {
|
||||
const distDir = join(packageRoot, "dist");
|
||||
mkdirSync(distDir, { recursive: true });
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
writeFileSync(join(distDir, `chunk-${index}.js`), "export {};\n", "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
describe("buildPublishedInstallScenarios", () => {
|
||||
it("uses a single fresh scenario for plain stable releases", () => {
|
||||
expect(buildPublishedInstallScenarios("2026.3.23")).toEqual([
|
||||
{
|
||||
name: "fresh-exact",
|
||||
installSpecs: ["openclaw@2026.3.23"],
|
||||
expectedVersion: "2026.3.23",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("adds a stable-to-correction upgrade scenario for correction releases", () => {
|
||||
expect(buildPublishedInstallScenarios("2026.3.23-2")).toEqual([
|
||||
{
|
||||
name: "fresh-exact",
|
||||
installSpecs: ["openclaw@2026.3.23-2"],
|
||||
expectedVersion: "2026.3.23-2",
|
||||
},
|
||||
{
|
||||
name: "upgrade-from-base-stable",
|
||||
installSpecs: ["openclaw@2026.3.23", "openclaw@2026.3.23-2"],
|
||||
expectedVersion: "2026.3.23-2",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("npm registry provenance verification", () => {
|
||||
const packageName = "openclaw";
|
||||
const version = "2026.3.23";
|
||||
const integrity = `sha512-${Buffer.from("registry integrity", "utf8").toString("base64")}`;
|
||||
const provenancePayload = {
|
||||
subject: [
|
||||
{
|
||||
name: `pkg:npm/${packageName}@${version}`,
|
||||
digest: {
|
||||
sha512: Buffer.from(integrity.slice("sha512-".length), "base64").toString("hex"),
|
||||
},
|
||||
},
|
||||
],
|
||||
predicate: {
|
||||
buildDefinition: {
|
||||
externalParameters: {
|
||||
workflow: {
|
||||
repository: "https://github.com/openclaw/openclaw",
|
||||
path: ".github/workflows/openclaw-npm-release.yml",
|
||||
ref: "refs/heads/release/2026.3.23",
|
||||
},
|
||||
},
|
||||
},
|
||||
runDetails: {
|
||||
builder: {
|
||||
id: "https://github.com/actions/runner/github-hosted",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("fetches npm registry JSON with bounded response handling", async () => {
|
||||
const fetchImpl = vi.fn(async (_url: string, init?: RequestInit) => {
|
||||
expect(init).toMatchObject({
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
redirect: "error",
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
return new Response(JSON.stringify({ ok: true }));
|
||||
});
|
||||
|
||||
await expect(
|
||||
fetchRegistryJson("https://registry.example/openclaw", {
|
||||
fetchImpl,
|
||||
timeoutMs: 1234,
|
||||
}),
|
||||
).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("bounds oversized npm registry response bodies", async () => {
|
||||
const fetchImpl = vi.fn(async () => {
|
||||
return new Response("x".repeat(65), {
|
||||
headers: { "content-length": "65" },
|
||||
});
|
||||
});
|
||||
|
||||
await expect(
|
||||
fetchRegistryJson("https://registry.example/openclaw", {
|
||||
fetchImpl,
|
||||
maxBodyBytes: 64,
|
||||
timeoutMs: 1234,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"npm registry https://registry.example/openclaw response body exceeded 64 bytes",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps npm registry timeouts active while reading response bodies", async () => {
|
||||
const fetchImpl = vi.fn(async () => {
|
||||
return new Response(new ReadableStream<Uint8Array>({ start() {} }));
|
||||
});
|
||||
|
||||
await expect(
|
||||
fetchRegistryJson("https://registry.example/openclaw", {
|
||||
fetchImpl,
|
||||
timeoutMs: 5,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"npm registry request timed out after 5ms: https://registry.example/openclaw",
|
||||
);
|
||||
});
|
||||
|
||||
it("verifies an npm registry signature against the matching public key", () => {
|
||||
const keys = generateKeyPairSync("ec", { namedCurve: "prime256v1" });
|
||||
const payload = `${packageName}@${version}:${integrity}`;
|
||||
const signature = sign("sha256", Buffer.from(payload, "utf8"), keys.privateKey).toString(
|
||||
"base64",
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
verifyNpmRegistrySignatures({
|
||||
packageName,
|
||||
version,
|
||||
integrity,
|
||||
signatures: [{ keyid: "test-key", sig: signature }],
|
||||
keys: [
|
||||
{
|
||||
keyid: "test-key",
|
||||
key: keys.publicKey.export({ format: "der", type: "spki" }).toString("base64"),
|
||||
},
|
||||
],
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("requires a trusted GitHub release identity for the exact SLSA provenance attestation", async () => {
|
||||
let verificationPolicy:
|
||||
| {
|
||||
certificateIdentityURI: string;
|
||||
certificateIssuer: string;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
await expect(
|
||||
verifyNpmProvenanceAttestation({
|
||||
packageName,
|
||||
version,
|
||||
integrity,
|
||||
attestations: [
|
||||
{
|
||||
predicateType: "https://slsa.dev/provenance/v1",
|
||||
bundle: {
|
||||
dsseEnvelope: {
|
||||
payload: Buffer.from(JSON.stringify(provenancePayload), "utf8").toString("base64"),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
verifyBundle: async (_bundle, policy) => {
|
||||
verificationPolicy = policy;
|
||||
},
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
expect(verificationPolicy).toEqual({
|
||||
certificateIssuer: "https://token.actions.githubusercontent.com",
|
||||
certificateIdentityURI:
|
||||
"https://github.com/openclaw/openclaw/.github/workflows/openclaw-npm-release.yml@refs/heads/release/2026.3.23",
|
||||
});
|
||||
|
||||
await expect(
|
||||
verifyNpmProvenanceAttestation({
|
||||
packageName,
|
||||
version,
|
||||
integrity,
|
||||
attestations: [
|
||||
{
|
||||
predicateType: "https://slsa.dev/provenance/v1",
|
||||
bundle: {
|
||||
dsseEnvelope: {
|
||||
payload: Buffer.from(
|
||||
JSON.stringify({
|
||||
...provenancePayload,
|
||||
subject: [{ name: "pkg:npm/openclaw@2026.3.24", digest: {} }],
|
||||
}),
|
||||
"utf8",
|
||||
).toString("base64"),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
verifyBundle: async () => undefined,
|
||||
}),
|
||||
).rejects.toThrow("does not match");
|
||||
});
|
||||
|
||||
it("rejects matching provenance from an untrusted source before Sigstore verification", async () => {
|
||||
let verificationCalls = 0;
|
||||
|
||||
await expect(
|
||||
verifyNpmProvenanceAttestation({
|
||||
packageName,
|
||||
version,
|
||||
integrity,
|
||||
attestations: [
|
||||
{
|
||||
predicateType: "https://slsa.dev/provenance/v1",
|
||||
bundle: {
|
||||
dsseEnvelope: {
|
||||
payload: Buffer.from(
|
||||
JSON.stringify({
|
||||
...provenancePayload,
|
||||
predicate: {
|
||||
...provenancePayload.predicate,
|
||||
buildDefinition: {
|
||||
externalParameters: {
|
||||
workflow: {
|
||||
...provenancePayload.predicate.buildDefinition.externalParameters
|
||||
.workflow,
|
||||
ref: "refs/heads/feature/untrusted",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
).toString("base64"),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
verifyBundle: async () => {
|
||||
verificationCalls += 1;
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("does not bind 2026.3.23 to the trusted OpenClaw GitHub release workflow");
|
||||
expect(verificationCalls).toBe(0);
|
||||
});
|
||||
|
||||
it("rejects a matching provenance payload when Sigstore cannot verify its bundle", async () => {
|
||||
await expect(
|
||||
verifyNpmProvenanceAttestation({
|
||||
packageName,
|
||||
version,
|
||||
integrity,
|
||||
attestations: [
|
||||
{
|
||||
predicateType: "https://slsa.dev/provenance/v1",
|
||||
bundle: {
|
||||
dsseEnvelope: {
|
||||
payload: Buffer.from(JSON.stringify(provenancePayload), "utf8").toString("base64"),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
verifyBundle: async () => {
|
||||
throw new Error("forged bundle");
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("failed Sigstore verification");
|
||||
});
|
||||
|
||||
it("retries incomplete registry metadata while npm publish propagates", async () => {
|
||||
let attempts = 0;
|
||||
const delays: number[] = [];
|
||||
|
||||
await expect(
|
||||
retryNpmRegistryProvenanceRead(
|
||||
async () => {
|
||||
attempts += 1;
|
||||
if (attempts < 3) {
|
||||
throw new Error(
|
||||
"npm registry provenance metadata is incomplete for openclaw@2026.3.23.",
|
||||
);
|
||||
}
|
||||
return "verified";
|
||||
},
|
||||
{
|
||||
attempts: 3,
|
||||
delay: async (delayMs) => {
|
||||
delays.push(delayMs);
|
||||
},
|
||||
},
|
||||
),
|
||||
).resolves.toBe("verified");
|
||||
expect(attempts).toBe(3);
|
||||
expect(delays).toEqual([1000, 2000]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPublishedInstallCommandArgs", () => {
|
||||
it("runs lifecycle scripts for published install verification", () => {
|
||||
const args = buildPublishedInstallCommandArgs("/tmp/openclaw-prefix", "openclaw@2026.4.10");
|
||||
|
||||
expect(args).toEqual([
|
||||
"install",
|
||||
"-g",
|
||||
"--prefix",
|
||||
"/tmp/openclaw-prefix",
|
||||
"openclaw@2026.4.10",
|
||||
"--no-fund",
|
||||
"--no-audit",
|
||||
]);
|
||||
expect(args).not.toContain("--ignore-scripts");
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectInstalledPackageErrors", () => {
|
||||
function makeInstalledPackageRoot(): string {
|
||||
return mkdtempSync(join(tmpdir(), "openclaw-postpublish-package-"));
|
||||
}
|
||||
|
||||
it("flags version mismatches", () => {
|
||||
const errors = collectInstalledPackageErrors({
|
||||
expectedVersion: "2026.3.23-2",
|
||||
installedVersion: "2026.3.23",
|
||||
packageRoot: "/tmp/empty-openclaw",
|
||||
});
|
||||
|
||||
expect(errors[0]).toBe(
|
||||
"installed package version mismatch: expected 2026.3.23-2, found 2026.3.23.",
|
||||
);
|
||||
});
|
||||
|
||||
it("requires runtime sidecars for bundled extensions included in the package", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writeFileSync(join(packageRoot, "package.json"), '{"version":"2026.3.23"}\n', "utf8");
|
||||
mkdirSync(join(packageRoot, "dist", "extensions", "telegram"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "extensions", "telegram", "package.json"),
|
||||
"{}\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledBundledRuntimeSidecarPaths(packageRoot)).toContain(
|
||||
"dist/extensions/telegram/runtime-api.js",
|
||||
);
|
||||
expect(
|
||||
collectInstalledPackageErrors({
|
||||
expectedVersion: "2026.3.23",
|
||||
installedVersion: "2026.3.23",
|
||||
packageRoot,
|
||||
}),
|
||||
).toContain(
|
||||
"installed package is missing required bundled runtime sidecar: dist/extensions/telegram/runtime-api.js",
|
||||
);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("surfaces invalid installed bundled extension manifests", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writeFileSync(join(packageRoot, "package.json"), '{"version":"2026.3.23"}\n', "utf8");
|
||||
mkdirSync(join(packageRoot, "dist", "extensions", "telegram"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "extensions", "telegram", "package.json"),
|
||||
"{not-json\n",
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "extensions", "telegram", "runtime-api.js"),
|
||||
"export {};\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const manifestErrors = collectInstalledBundledExtensionManifestErrors(packageRoot);
|
||||
expect(manifestErrors).toHaveLength(1);
|
||||
expect(manifestErrors[0]).toContain(
|
||||
"installed bundled extension manifest invalid: failed to parse",
|
||||
);
|
||||
expect(manifestErrors[0]).toContain("dist/extensions/telegram/package.json");
|
||||
|
||||
expect(
|
||||
collectInstalledPackageErrors({
|
||||
expectedVersion: "2026.3.23",
|
||||
installedVersion: "2026.3.23",
|
||||
packageRoot,
|
||||
}),
|
||||
).toContain(manifestErrors[0]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectInstalledContextEngineRuntimeErrors", () => {
|
||||
function makeInstalledPackageRoot(): string {
|
||||
return mkdtempSync(join(tmpdir(), "openclaw-postpublish-context-engine-"));
|
||||
}
|
||||
|
||||
it("rejects packaged bundles with unresolved legacy context engine runtime loaders", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "runtime-plugins-BUG.js"),
|
||||
'throw new Error("Failed to load legacy context engine runtime.");\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledContextEngineRuntimeErrors(packageRoot)).toEqual([
|
||||
"installed package includes unresolved legacy context engine runtime loader; rebuild with a bundler-traceable LegacyContextEngine import.",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts packaged bundles that inline the legacy context engine registration", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "runtime-plugins-OK.js"),
|
||||
"registerContextEngineForOwner('legacy', async () => new LegacyContextEngine());\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledContextEngineRuntimeErrors(packageRoot)).toStrictEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses unbounded packaged dist scans", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writeDistJavaScriptFiles(packageRoot, INSTALLED_ROOT_DIST_JS_FILE_SCAN_LIMIT + 1);
|
||||
|
||||
expect(collectInstalledContextEngineRuntimeErrors(packageRoot)).toEqual([
|
||||
`installed package dist contains more than ${INSTALLED_ROOT_DIST_JS_FILE_SCAN_LIMIT} JavaScript files; refusing to scan unbounded package contents.`,
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectInstalledPluginSdkZodArtifactErrors", () => {
|
||||
function withInstalledPackageRoot(run: (packageRoot: string) => void): void {
|
||||
const packageRoot = mkdtempSync(join(tmpdir(), "openclaw-postpublish-zod-sdk-"));
|
||||
try {
|
||||
run(packageRoot);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function writeInstalledFile(packageRoot: string, relativePath: string, contents: string): void {
|
||||
const filePath = join(packageRoot, ...relativePath.split("/"));
|
||||
mkdirSync(dirname(filePath), { recursive: true });
|
||||
writeFileSync(filePath, contents, "utf8");
|
||||
}
|
||||
|
||||
it("requires the plugin-sdk zod artifact", () => {
|
||||
withInstalledPackageRoot((packageRoot) => {
|
||||
expect(collectInstalledPluginSdkZodArtifactErrors(packageRoot)).toEqual([
|
||||
"installed package is missing required plugin SDK artifact: dist/plugin-sdk/zod.js",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects plugin-sdk zod artifacts with a bare zod export", () => {
|
||||
withInstalledPackageRoot((packageRoot) => {
|
||||
writeInstalledFile(
|
||||
packageRoot,
|
||||
"dist/plugin-sdk/zod.js",
|
||||
'import "../zod-D2c0iocA.js";\nexport * from "zod";\n',
|
||||
);
|
||||
|
||||
expect(collectInstalledPluginSdkZodArtifactErrors(packageRoot)).toEqual([
|
||||
"installed package plugin SDK zod artifact must be self-contained but dist/plugin-sdk/zod.js imports zod.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects plugin-sdk zod artifacts when a reachable local chunk imports zod", () => {
|
||||
withInstalledPackageRoot((packageRoot) => {
|
||||
writeInstalledFile(
|
||||
packageRoot,
|
||||
"dist/plugin-sdk/zod.js",
|
||||
'export { z } from "../zod-D2c0iocA.js";\n',
|
||||
);
|
||||
writeInstalledFile(
|
||||
packageRoot,
|
||||
"dist/zod-D2c0iocA.js",
|
||||
'import * as zodCore from "zod/v4/core";\nexport const z = zodCore;\n',
|
||||
);
|
||||
|
||||
expect(collectInstalledPluginSdkZodArtifactErrors(packageRoot)).toEqual([
|
||||
"installed package plugin SDK zod artifact must be self-contained but dist/zod-D2c0iocA.js imports zod/v4/core.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts plugin-sdk zod artifacts that only import package-local chunks", () => {
|
||||
withInstalledPackageRoot((packageRoot) => {
|
||||
writeInstalledFile(
|
||||
packageRoot,
|
||||
"dist/plugin-sdk/zod.js",
|
||||
'export { z } from "../zod-D2c0iocA.js";\n',
|
||||
);
|
||||
writeInstalledFile(packageRoot, "dist/zod-D2c0iocA.js", "export const z = {};\n");
|
||||
|
||||
expect(collectInstalledPluginSdkZodArtifactErrors(packageRoot)).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeInstalledBinaryVersion", () => {
|
||||
it("accepts decorated CLI version output", () => {
|
||||
expect(normalizeInstalledBinaryVersion("OpenClaw 2026.4.8 (9ece252)")).toBe("2026.4.8");
|
||||
expect(normalizeInstalledBinaryVersion("OpenClaw 2026.4.8-beta.1 (9ece252)")).toBe(
|
||||
"2026.4.8-beta.1",
|
||||
);
|
||||
expect(normalizeInstalledBinaryVersion("OpenClaw 2026.4.8-alpha.1 (9ece252)")).toBe(
|
||||
"2026.4.8-alpha.1",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveInstalledBinaryPath", () => {
|
||||
it("uses the Unix global bin path on non-Windows platforms", () => {
|
||||
expect(resolveInstalledBinaryPath("/tmp/openclaw-prefix", "darwin")).toBe(
|
||||
"/tmp/openclaw-prefix/bin/openclaw",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the Windows npm shim path on win32", () => {
|
||||
expect(resolveInstalledBinaryPath("C:/openclaw-prefix", "win32")).toBe(
|
||||
"C:\\openclaw-prefix\\openclaw.cmd",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveInstalledBinaryCommandInvocation", () => {
|
||||
it("runs the Unix installed binary directly", () => {
|
||||
expect(
|
||||
resolveInstalledBinaryCommandInvocation("/tmp/openclaw-prefix", ["--version"], {
|
||||
platform: "linux",
|
||||
}),
|
||||
).toEqual({
|
||||
command: "/tmp/openclaw-prefix/bin/openclaw",
|
||||
args: ["--version"],
|
||||
});
|
||||
});
|
||||
|
||||
it("wraps the Windows installed npm shim without Node shell argv", () => {
|
||||
expect(
|
||||
resolveInstalledBinaryCommandInvocation(
|
||||
"C:/openclaw prefix",
|
||||
["agent", "--message", "hello world"],
|
||||
{
|
||||
comSpec: "C:\\Windows\\System32\\cmd.exe",
|
||||
platform: "win32",
|
||||
},
|
||||
),
|
||||
).toEqual({
|
||||
command: "C:\\Windows\\System32\\cmd.exe",
|
||||
args: [
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
'""C:\\openclaw prefix\\openclaw.cmd" agent --message "hello world""',
|
||||
],
|
||||
windowsVerbatimArguments: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectInstalledRootDependencyManifestErrors", () => {
|
||||
function makeInstalledPackageRoot(): string {
|
||||
return mkdtempSync(join(tmpdir(), "openclaw-postpublish-root-deps-"));
|
||||
}
|
||||
|
||||
function writePackageFile(root: string, relativePath: string, value: unknown): void {
|
||||
const fullPath = join(root, relativePath);
|
||||
mkdirSync(dirname(fullPath), { recursive: true });
|
||||
writeFileSync(fullPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
it("flags root dist imports whose declared runtime package name is missing", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.22",
|
||||
dependencies: {},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "typebox-CXXonh2u.js"),
|
||||
'import { Type } from "typebox";\nexport { Type };\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
"installed package root is missing declared runtime dependency 'typebox' for dist importers: typebox-CXXonh2u.js. Add it to package.json dependencies/optionalDependencies.",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts root dist imports when the runtime package name is declared", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.22",
|
||||
dependencies: {
|
||||
typebox: "1.1.28",
|
||||
},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "typebox-CXXonh2u.js"),
|
||||
'import { Type } from "typebox";\nexport { Type };\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toStrictEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts optional or externalized runtime imports", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.22",
|
||||
dependencies: {},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "optional-runtime.js"),
|
||||
['await import("@a2ui/markdown-it");', 'await import("@lancedb/lancedb");', ""].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "externalized-plugin-runtime.js"),
|
||||
[
|
||||
'import * as lark from "@larksuiteoapi/node-sdk";',
|
||||
'import prism from "prism-media";',
|
||||
"export { lark, prism };",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
mkdirSync(join(packageRoot, "dist", "plugin-sdk"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "plugin-sdk/channel-test-helpers.js"),
|
||||
'import { expect, it } from "vitest";\nexport { expect, it };\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toStrictEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("flags undeclared imports from mjs and cjs root dist files", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.22",
|
||||
dependencies: {},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "esm-entry.mjs"),
|
||||
'export { value } from "mjs-only";\n',
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "cjs-entry.cjs"),
|
||||
'const cjsOnly = require("cjs-only");\nmodule.exports = cjsOnly;\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
"installed package root is missing declared runtime dependency 'cjs-only' for dist importers: cjs-entry.cjs. Add it to package.json dependencies/optionalDependencies.",
|
||||
"installed package root is missing declared runtime dependency 'mjs-only' for dist importers: esm-entry.mjs. Add it to package.json dependencies/optionalDependencies.",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores import-like text inside comments", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.22",
|
||||
dependencies: {},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "comment-only.js"),
|
||||
[
|
||||
'// import "fake-package";',
|
||||
'/* require("fake-package-two"); */',
|
||||
"export const ok = true;",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toStrictEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores import-like text inside string literals", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.22",
|
||||
dependencies: {},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "string-only.js"),
|
||||
[
|
||||
"export const help = \"run import('fake-package') after setup\";",
|
||||
'export const note = "from \\"fake-package-two\\"";',
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toStrictEqual([]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("returns a structured error when installed package.json is invalid", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(join(packageRoot, "package.json"), "{not-json\n", "utf8");
|
||||
|
||||
const errors = collectInstalledRootDependencyManifestErrors(packageRoot);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0]?.startsWith("installed package.json could not be parsed:")).toBe(true);
|
||||
expect(errors[0]?.endsWith(".")).toBe(true);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses oversized root dist files", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.22",
|
||||
dependencies: {},
|
||||
});
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "oversized.js"),
|
||||
"x".repeat(6 * 1024 * 1024 + 1),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
"installed package root dist file 'oversized.js' is invalid or exceeds 6291456 bytes.",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses unbounded root dist dependency scans", () => {
|
||||
const packageRoot = makeInstalledPackageRoot();
|
||||
|
||||
try {
|
||||
writePackageFile(packageRoot, "package.json", {
|
||||
version: "2026.4.22",
|
||||
dependencies: {},
|
||||
});
|
||||
writeDistJavaScriptFiles(packageRoot, INSTALLED_ROOT_DIST_JS_FILE_SCAN_LIMIT + 1);
|
||||
|
||||
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([
|
||||
`installed package root dist contains more than ${INSTALLED_ROOT_DIST_JS_FILE_SCAN_LIMIT} JavaScript files; refusing to scan unbounded package contents.`,
|
||||
]);
|
||||
} finally {
|
||||
rmSync(packageRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user