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:
759
test/plugin-npm-release.test.ts
Normal file
759
test/plugin-npm-release.test.ts
Normal file
@@ -0,0 +1,759 @@
|
||||
// Plugin npm release tests validate plugin npm release artifacts.
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { bundledPluginFile, bundledPluginRoot } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { collectClawHubPublishablePluginPackages } from "../scripts/lib/plugin-clawhub-release.ts";
|
||||
import {
|
||||
collectChangedExtensionIdsFromPaths,
|
||||
collectPluginReleaseDependencyFreshnessErrors,
|
||||
collectPluginReleaseVersionFloorErrors,
|
||||
collectPublishablePluginPackages,
|
||||
collectPublishablePluginPackageErrors,
|
||||
OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
||||
parsePluginReleaseArgs,
|
||||
parsePluginReleaseSelection,
|
||||
parsePluginReleaseSelectionMode,
|
||||
resolveChangedPublishablePluginPackages,
|
||||
resolveSelectedPublishablePluginPackages,
|
||||
type PublishablePluginPackage,
|
||||
} from "../scripts/lib/plugin-npm-release.ts";
|
||||
import { cleanupTempDirs, makeTempRepoRoot, writeJsonFile } from "./helpers/temp-repo.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTempDirs(tempDirs);
|
||||
});
|
||||
|
||||
describe("parsePluginReleaseSelection", () => {
|
||||
it("returns an empty list for blank input", () => {
|
||||
expect(parsePluginReleaseSelection("")).toStrictEqual([]);
|
||||
expect(parsePluginReleaseSelection(" ")).toStrictEqual([]);
|
||||
expect(parsePluginReleaseSelection(undefined)).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("dedupes and sorts comma or whitespace separated package names", () => {
|
||||
expect(
|
||||
parsePluginReleaseSelection(" @openclaw/zalo, @openclaw/feishu @openclaw/zalo "),
|
||||
).toEqual(["@openclaw/feishu", "@openclaw/zalo"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parsePluginReleaseSelectionMode", () => {
|
||||
it("accepts the supported explicit selection modes", () => {
|
||||
expect(parsePluginReleaseSelectionMode("selected")).toBe("selected");
|
||||
expect(parsePluginReleaseSelectionMode("all-publishable")).toBe("all-publishable");
|
||||
});
|
||||
|
||||
it("rejects unsupported selection modes", () => {
|
||||
expect(() => parsePluginReleaseSelectionMode("all")).toThrowError(
|
||||
'Unknown selection mode: all. Expected "selected" or "all-publishable".',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parsePluginReleaseArgs", () => {
|
||||
it("rejects blank explicit plugin selections", () => {
|
||||
expect(() => parsePluginReleaseArgs(["--plugins", " "])).toThrowError(
|
||||
"`--plugins` must include at least one package name.",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects flags where option values are required", () => {
|
||||
for (const { args, message } of [
|
||||
{ args: ["--plugins", "--base-ref"], message: "--plugins requires a value." },
|
||||
{
|
||||
args: ["--selection-mode", "--plugins"],
|
||||
message: "--selection-mode requires a value.",
|
||||
},
|
||||
{
|
||||
args: ["--base-ref", "--head-ref", "main"],
|
||||
message: "--base-ref requires a value.",
|
||||
},
|
||||
{
|
||||
args: ["--head-ref", "--base-ref", "main"],
|
||||
message: "--head-ref requires a value.",
|
||||
},
|
||||
]) {
|
||||
expect(() => parsePluginReleaseArgs(args)).toThrowError(message);
|
||||
}
|
||||
});
|
||||
|
||||
it("requires plugin names for selected explicit publish mode", () => {
|
||||
expect(() => parsePluginReleaseArgs(["--selection-mode", "selected"])).toThrowError(
|
||||
"`--selection-mode selected` requires `--plugins`.",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects plugin names when all-publishable mode is selected", () => {
|
||||
expect(() =>
|
||||
parsePluginReleaseArgs([
|
||||
"--selection-mode",
|
||||
"all-publishable",
|
||||
"--plugins",
|
||||
"@openclaw/zalo",
|
||||
]),
|
||||
).toThrowError("`--selection-mode all-publishable` must not be combined with `--plugins`.");
|
||||
});
|
||||
|
||||
it("parses explicit all-publishable mode", () => {
|
||||
expect(parsePluginReleaseArgs(["--selection-mode", "all-publishable"])).toEqual({
|
||||
baseRef: undefined,
|
||||
headRef: undefined,
|
||||
selectionMode: "all-publishable",
|
||||
selection: [],
|
||||
pluginsFlagProvided: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function externalPluginContract(version: string) {
|
||||
return {
|
||||
compat: {
|
||||
pluginApi: `>=${version}`,
|
||||
},
|
||||
build: {
|
||||
openclawVersion: version,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function writePluginReadme(repoDir: string, extensionId: string): void {
|
||||
const packageDir = join(repoDir, "extensions", extensionId);
|
||||
mkdirSync(packageDir, { recursive: true });
|
||||
writeFileSync(join(packageDir, "README.md"), `# ${extensionId}\n`);
|
||||
}
|
||||
|
||||
describe("collectPublishablePluginPackageErrors", () => {
|
||||
it("accepts a valid publishable plugin package candidate", () => {
|
||||
expect(
|
||||
collectPublishablePluginPackageErrors({
|
||||
extensionId: "zalo",
|
||||
packageDir: bundledPluginRoot("zalo"),
|
||||
readmeText: "# Zalo\n",
|
||||
packageJson: {
|
||||
name: "@openclaw/zalo",
|
||||
version: "2026.3.15",
|
||||
type: "module",
|
||||
repository: {
|
||||
type: "git",
|
||||
url: OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
||||
},
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
...externalPluginContract("2026.3.15"),
|
||||
install: {
|
||||
npmSpec: "@openclaw/zalo",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("flags invalid publishable plugin metadata", () => {
|
||||
expect(
|
||||
collectPublishablePluginPackageErrors({
|
||||
extensionId: "broken",
|
||||
packageDir: bundledPluginRoot("broken"),
|
||||
readmeText: "# Broken\n",
|
||||
packageJson: {
|
||||
name: "broken",
|
||||
version: "latest",
|
||||
private: true,
|
||||
openclaw: {
|
||||
extensions: [""],
|
||||
...externalPluginContract("2026.3.15"),
|
||||
install: {
|
||||
npmSpec: " ",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual([
|
||||
'package name must start with "@openclaw/"; found "broken".',
|
||||
"package.json private must not be true.",
|
||||
'package.json type must be "module" so built .js runtime entries load as ESM.',
|
||||
`package.json repository.url must be "${OPENCLAW_PLUGIN_NPM_REPOSITORY_URL}" so npm provenance can validate GitHub trusted publishing; found "<missing>".`,
|
||||
'package.json version must match YYYY.M.PATCH, YYYY.M.PATCH-N, YYYY.M.PATCH-alpha.N, or YYYY.M.PATCH-beta.N; found "latest".',
|
||||
"openclaw.extensions must contain only non-empty strings.",
|
||||
"openclaw.install.npmSpec must be a non-empty string for publishable plugins.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("requires the GitHub repository URL npm provenance validates for trusted publishing", () => {
|
||||
expect(
|
||||
collectPublishablePluginPackageErrors({
|
||||
extensionId: "twitch",
|
||||
packageDir: bundledPluginRoot("twitch"),
|
||||
readmeText: "# Twitch\n",
|
||||
packageJson: {
|
||||
name: "@openclaw/twitch",
|
||||
version: "2026.5.1-beta.1",
|
||||
type: "module",
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
...externalPluginContract("2026.5.1-beta.1"),
|
||||
install: {
|
||||
npmSpec: "@openclaw/twitch",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual([
|
||||
`package.json repository.url must be "${OPENCLAW_PLUGIN_NPM_REPOSITORY_URL}" so npm provenance can validate GitHub trusted publishing; found "<missing>".`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("requires npm install metadata for publishable plugins", () => {
|
||||
expect(
|
||||
collectPublishablePluginPackageErrors({
|
||||
extensionId: "voice-call",
|
||||
packageDir: bundledPluginRoot("voice-call"),
|
||||
readmeText: "# Voice call\n",
|
||||
packageJson: {
|
||||
name: "@openclaw/voice-call",
|
||||
version: "2026.5.1-beta.1",
|
||||
type: "module",
|
||||
repository: {
|
||||
type: "git",
|
||||
url: OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
||||
},
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
...externalPluginContract("2026.5.1-beta.1"),
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual(["openclaw.install.npmSpec must be a non-empty string for publishable plugins."]);
|
||||
});
|
||||
|
||||
it("requires the external plugin package compatibility contract for npm publish", () => {
|
||||
expect(
|
||||
collectPublishablePluginPackageErrors({
|
||||
extensionId: "voice-call",
|
||||
packageDir: bundledPluginRoot("voice-call"),
|
||||
readmeText: "# Voice call\n",
|
||||
packageJson: {
|
||||
name: "@openclaw/voice-call",
|
||||
version: "2026.5.1-beta.1",
|
||||
type: "module",
|
||||
repository: {
|
||||
type: "git",
|
||||
url: OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
||||
},
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
install: {
|
||||
npmSpec: "@openclaw/voice-call",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual([
|
||||
"openclaw.compat.pluginApi is required for external code plugin packages.",
|
||||
"openclaw.build.openclawVersion is required for external code plugin packages.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("requires package documentation before publishing", () => {
|
||||
expect(
|
||||
collectPublishablePluginPackageErrors({
|
||||
extensionId: "zalo",
|
||||
packageDir: bundledPluginRoot("zalo"),
|
||||
readmeText: " \n",
|
||||
packageJson: {
|
||||
name: "@openclaw/zalo",
|
||||
version: "2026.3.15",
|
||||
type: "module",
|
||||
repository: {
|
||||
type: "git",
|
||||
url: OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
||||
},
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
...externalPluginContract("2026.3.15"),
|
||||
install: {
|
||||
npmSpec: "@openclaw/zalo",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual(["README.md must exist and contain package documentation."]);
|
||||
});
|
||||
|
||||
it("requires latest-release dependencies to name exact runtime dependencies", () => {
|
||||
expect(
|
||||
collectPublishablePluginPackageErrors({
|
||||
extensionId: "codex",
|
||||
packageDir: bundledPluginRoot("codex"),
|
||||
readmeText: "# Codex\n",
|
||||
packageJson: {
|
||||
name: "@openclaw/codex",
|
||||
version: "2026.6.11",
|
||||
type: "module",
|
||||
repository: {
|
||||
type: "git",
|
||||
url: OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
||||
},
|
||||
dependencies: {
|
||||
"@openai/codex": "0.142.5",
|
||||
},
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
...externalPluginContract("2026.6.11"),
|
||||
install: {
|
||||
npmSpec: "@openclaw/codex",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
requireLatestDependencies: ["@openai/codex", "@openai/codex", "missing"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual([
|
||||
'openclaw.release.requireLatestDependencies must not contain duplicate package names; found "@openai/codex".',
|
||||
'openclaw.release.requireLatestDependencies must reference package.json dependencies or optionalDependencies; "missing" is not a runtime dependency.',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectPluginReleaseVersionFloorErrors", () => {
|
||||
it("blocks selected plugin stable and beta releases below the June 2026 floor", () => {
|
||||
expect(
|
||||
collectPluginReleaseVersionFloorErrors([
|
||||
{
|
||||
packageName: "@openclaw/demo",
|
||||
version: "2026.6.4-beta.1",
|
||||
},
|
||||
]),
|
||||
).toEqual([
|
||||
'@openclaw/demo@2026.6.4-beta.1: June 2026 stable and beta release trains must use patch 5 or higher because 2026.6.5-beta.1 is already published; found "2026.6.4-beta.1".',
|
||||
]);
|
||||
});
|
||||
|
||||
it("allows alpha compatibility and patch-floor plugin releases", () => {
|
||||
expect(
|
||||
collectPluginReleaseVersionFloorErrors([
|
||||
{
|
||||
packageName: "@openclaw/demo",
|
||||
version: "2026.6.4-alpha.1",
|
||||
},
|
||||
{
|
||||
packageName: "@openclaw/demo",
|
||||
version: "2026.6.5-beta.2",
|
||||
},
|
||||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectPluginReleaseDependencyFreshnessErrors", () => {
|
||||
const plugin: PublishablePluginPackage = {
|
||||
extensionId: "codex",
|
||||
packageDir: "extensions/codex",
|
||||
packageName: "@openclaw/codex",
|
||||
version: "2026.6.11",
|
||||
channel: "stable",
|
||||
publishTag: "latest",
|
||||
requiredLatestDependencies: [
|
||||
{
|
||||
packageName: "@openai/codex",
|
||||
version: "0.139.0",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it("rejects release dependencies older than the npm latest dist-tag", () => {
|
||||
expect(collectPluginReleaseDependencyFreshnessErrors([plugin], () => "0.142.5")).toEqual([
|
||||
'@openclaw/codex@2026.6.11: @openai/codex must match npm latest for release; found "0.139.0", latest is "0.142.5".',
|
||||
]);
|
||||
});
|
||||
|
||||
it("accepts release dependencies matching the npm latest dist-tag", () => {
|
||||
expect(
|
||||
collectPluginReleaseDependencyFreshnessErrors(
|
||||
[
|
||||
{
|
||||
...plugin,
|
||||
requiredLatestDependencies: [
|
||||
{
|
||||
packageName: "@openai/codex",
|
||||
version: "0.142.5",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
() => "0.142.5",
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("fails closed when npm latest cannot be resolved", () => {
|
||||
expect(
|
||||
collectPluginReleaseDependencyFreshnessErrors([plugin], () => {
|
||||
throw new Error("registry unavailable");
|
||||
}),
|
||||
).toEqual([
|
||||
"@openclaw/codex@2026.6.11: could not resolve npm latest for @openai/codex: registry unavailable",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectPublishablePluginPackages", () => {
|
||||
it("keeps publishable plugin dist trees out of the core npm package unless bundled", () => {
|
||||
const corePackageRuntimePluginIds = new Set(["discord"]);
|
||||
const rootPackage = JSON.parse(readFileSync("package.json", "utf8")) as {
|
||||
files?: unknown;
|
||||
};
|
||||
const packageFiles = new Set(Array.isArray(rootPackage.files) ? rootPackage.files : []);
|
||||
const publishablePlugins = [
|
||||
...collectPublishablePluginPackages(),
|
||||
...collectClawHubPublishablePluginPackages(),
|
||||
];
|
||||
for (const plugin of publishablePlugins) {
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(join(plugin.packageDir, "package.json"), "utf8"),
|
||||
) as {
|
||||
openclaw?: {
|
||||
build?: {
|
||||
bundledDist?: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
if (packageJson.openclaw?.build?.bundledDist === true) {
|
||||
corePackageRuntimePluginIds.add(plugin.extensionId);
|
||||
}
|
||||
}
|
||||
const missingExclusions = Array.from(
|
||||
new Set(
|
||||
publishablePlugins
|
||||
.filter((plugin) => !corePackageRuntimePluginIds.has(plugin.extensionId))
|
||||
.map((plugin) => `!dist/extensions/${plugin.extensionId}/**`),
|
||||
),
|
||||
).filter((entry) => !packageFiles.has(entry));
|
||||
|
||||
expect(missingExclusions).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("collects publishable npm plugins from extension package manifests", () => {
|
||||
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-release-");
|
||||
mkdirSync(join(repoDir, "extensions", "demo-plugin"), { recursive: true });
|
||||
writePluginReadme(repoDir, "demo-plugin");
|
||||
writeJsonFile(join(repoDir, "extensions", "demo-plugin", "package.json"), {
|
||||
name: "@openclaw/demo-plugin",
|
||||
version: "2026.4.10",
|
||||
type: "module",
|
||||
repository: {
|
||||
type: "git",
|
||||
url: OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
||||
},
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
...externalPluginContract("2026.4.10"),
|
||||
install: {
|
||||
npmSpec: "@openclaw/demo-plugin",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(collectPublishablePluginPackages(repoDir)).toEqual([
|
||||
{
|
||||
extensionId: "demo-plugin",
|
||||
packageDir: "extensions/demo-plugin",
|
||||
packageName: "@openclaw/demo-plugin",
|
||||
version: "2026.4.10",
|
||||
channel: "stable",
|
||||
publishTag: "latest",
|
||||
installNpmSpec: "@openclaw/demo-plugin",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("collects exact release dependencies that must match npm latest", () => {
|
||||
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-release-");
|
||||
mkdirSync(join(repoDir, "extensions", "demo-plugin"), { recursive: true });
|
||||
writePluginReadme(repoDir, "demo-plugin");
|
||||
writeJsonFile(join(repoDir, "extensions", "demo-plugin", "package.json"), {
|
||||
name: "@openclaw/demo-plugin",
|
||||
version: "2026.4.10",
|
||||
type: "module",
|
||||
repository: {
|
||||
type: "git",
|
||||
url: OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
||||
},
|
||||
dependencies: {
|
||||
"demo-runtime": "1.2.3",
|
||||
},
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
...externalPluginContract("2026.4.10"),
|
||||
install: {
|
||||
npmSpec: "@openclaw/demo-plugin",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
requireLatestDependencies: ["demo-runtime"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(collectPublishablePluginPackages(repoDir)).toEqual([
|
||||
{
|
||||
extensionId: "demo-plugin",
|
||||
packageDir: "extensions/demo-plugin",
|
||||
packageName: "@openclaw/demo-plugin",
|
||||
version: "2026.4.10",
|
||||
channel: "stable",
|
||||
publishTag: "latest",
|
||||
installNpmSpec: "@openclaw/demo-plugin",
|
||||
requiredLatestDependencies: [
|
||||
{
|
||||
packageName: "demo-runtime",
|
||||
version: "1.2.3",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not validate unselected publishable plugin manifests", () => {
|
||||
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-release-");
|
||||
mkdirSync(join(repoDir, "extensions", "demo-plugin"), { recursive: true });
|
||||
writePluginReadme(repoDir, "demo-plugin");
|
||||
writeJsonFile(join(repoDir, "extensions", "demo-plugin", "package.json"), {
|
||||
name: "@openclaw/demo-plugin",
|
||||
version: "2026.4.10-beta.1",
|
||||
type: "module",
|
||||
repository: {
|
||||
type: "git",
|
||||
url: OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
||||
},
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
...externalPluginContract("2026.4.10-beta.1"),
|
||||
install: {
|
||||
npmSpec: "@openclaw/demo-plugin",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
mkdirSync(join(repoDir, "extensions", "private-plugin"), { recursive: true });
|
||||
writeJsonFile(join(repoDir, "extensions", "private-plugin", "package.json"), {
|
||||
name: "@openclaw/private-plugin",
|
||||
version: "2026.4.10-beta.1",
|
||||
private: true,
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
...externalPluginContract("2026.4.10-beta.1"),
|
||||
install: {
|
||||
npmSpec: "@openclaw/private-plugin",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
collectPublishablePluginPackages(repoDir, {
|
||||
packageNames: ["@openclaw/demo-plugin"],
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
extensionId: "demo-plugin",
|
||||
packageDir: "extensions/demo-plugin",
|
||||
installNpmSpec: "@openclaw/demo-plugin",
|
||||
channel: "beta",
|
||||
packageName: "@openclaw/demo-plugin",
|
||||
publishTag: "beta",
|
||||
version: "2026.4.10-beta.1",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats an explicit empty extension filter as no candidates", () => {
|
||||
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-release-");
|
||||
mkdirSync(join(repoDir, "extensions", "private-plugin"), { recursive: true });
|
||||
writeJsonFile(join(repoDir, "extensions", "private-plugin", "package.json"), {
|
||||
name: "@openclaw/private-plugin",
|
||||
version: "2026.4.10-beta.1",
|
||||
private: true,
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
...externalPluginContract("2026.4.10-beta.1"),
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
collectPublishablePluginPackages(repoDir, {
|
||||
extensionIds: [],
|
||||
}),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("publishes alpha plugin packages to the alpha dist-tag", () => {
|
||||
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-release-");
|
||||
mkdirSync(join(repoDir, "extensions", "demo-plugin"), { recursive: true });
|
||||
writePluginReadme(repoDir, "demo-plugin");
|
||||
writeJsonFile(join(repoDir, "extensions", "demo-plugin", "package.json"), {
|
||||
name: "@openclaw/demo-plugin",
|
||||
version: "2026.4.10-alpha.1",
|
||||
type: "module",
|
||||
repository: {
|
||||
type: "git",
|
||||
url: OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
||||
},
|
||||
openclaw: {
|
||||
extensions: ["./index.ts"],
|
||||
...externalPluginContract("2026.4.10-alpha.1"),
|
||||
install: {
|
||||
npmSpec: "@openclaw/demo-plugin",
|
||||
},
|
||||
release: {
|
||||
publishToNpm: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(collectPublishablePluginPackages(repoDir)).toEqual([
|
||||
{
|
||||
extensionId: "demo-plugin",
|
||||
packageDir: "extensions/demo-plugin",
|
||||
installNpmSpec: "@openclaw/demo-plugin",
|
||||
packageName: "@openclaw/demo-plugin",
|
||||
channel: "alpha",
|
||||
publishTag: "alpha",
|
||||
version: "2026.4.10-alpha.1",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSelectedPublishablePluginPackages", () => {
|
||||
const publishablePlugins: PublishablePluginPackage[] = [
|
||||
{
|
||||
extensionId: "feishu",
|
||||
packageDir: bundledPluginRoot("feishu"),
|
||||
packageName: "@openclaw/feishu",
|
||||
version: "2026.3.15",
|
||||
channel: "stable",
|
||||
publishTag: "latest",
|
||||
},
|
||||
{
|
||||
extensionId: "zalo",
|
||||
packageDir: bundledPluginRoot("zalo"),
|
||||
packageName: "@openclaw/zalo",
|
||||
version: "2026.3.15-beta.1",
|
||||
channel: "beta",
|
||||
publishTag: "beta",
|
||||
},
|
||||
];
|
||||
|
||||
it("returns all publishable plugins when no selection is provided", () => {
|
||||
expect(
|
||||
resolveSelectedPublishablePluginPackages({
|
||||
plugins: publishablePlugins,
|
||||
selection: [],
|
||||
}),
|
||||
).toEqual(publishablePlugins);
|
||||
});
|
||||
|
||||
it("filters by selected publishable package names", () => {
|
||||
expect(
|
||||
resolveSelectedPublishablePluginPackages({
|
||||
plugins: publishablePlugins,
|
||||
selection: ["@openclaw/zalo"],
|
||||
}),
|
||||
).toEqual([publishablePlugins[1]]);
|
||||
});
|
||||
|
||||
it("throws when the selection contains an unknown package name", () => {
|
||||
expect(() =>
|
||||
resolveSelectedPublishablePluginPackages({
|
||||
plugins: publishablePlugins,
|
||||
selection: ["@openclaw/missing"],
|
||||
}),
|
||||
).toThrowError("Unknown or non-publishable plugin package selection: @openclaw/missing.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectChangedExtensionIdsFromPaths", () => {
|
||||
it("extracts unique extension ids from changed extension paths", () => {
|
||||
expect(
|
||||
collectChangedExtensionIdsFromPaths([
|
||||
bundledPluginFile("zalo", "index.ts"),
|
||||
bundledPluginFile("zalo", "package.json"),
|
||||
bundledPluginFile("feishu", "src/client.ts"),
|
||||
"docs/reference/RELEASING.md",
|
||||
]),
|
||||
).toEqual(["feishu", "zalo"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveChangedPublishablePluginPackages", () => {
|
||||
const publishablePlugins: PublishablePluginPackage[] = [
|
||||
{
|
||||
extensionId: "feishu",
|
||||
packageDir: bundledPluginRoot("feishu"),
|
||||
packageName: "@openclaw/feishu",
|
||||
version: "2026.3.15",
|
||||
channel: "stable",
|
||||
publishTag: "latest",
|
||||
},
|
||||
{
|
||||
extensionId: "zalo",
|
||||
packageDir: bundledPluginRoot("zalo"),
|
||||
packageName: "@openclaw/zalo",
|
||||
version: "2026.3.15-beta.1",
|
||||
channel: "beta",
|
||||
publishTag: "beta",
|
||||
},
|
||||
];
|
||||
|
||||
it("returns only changed publishable plugins", () => {
|
||||
expect(
|
||||
resolveChangedPublishablePluginPackages({
|
||||
plugins: publishablePlugins,
|
||||
changedExtensionIds: ["zalo"],
|
||||
}),
|
||||
).toEqual([publishablePlugins[1]]);
|
||||
});
|
||||
|
||||
it("returns an empty list when no publishable plugins changed", () => {
|
||||
expect(
|
||||
resolveChangedPublishablePluginPackages({
|
||||
plugins: publishablePlugins,
|
||||
changedExtensionIds: [],
|
||||
}),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user