Files
openclaw/src/plugins/bundled-plugin-naming.test.ts
Onur Solmaz b79560c7f0 ACP: fully rename acpx plugin (#52404)
* ACP: rename acpx plugin package

* ACP: fully rename acpx plugin

* ACP: remove old acpx paths

* Docs: add bundled plugin naming guardrails

* Docs: keep plugin naming guardrails internal

* ACP: keep acpx plugin id stable

* ACP: drop old acpx-plugin tree
2026-03-22 19:33:31 +01:00

144 lines
4.8 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
type PluginManifestShape = {
id?: unknown;
};
type OpenClawPackageShape = {
name?: unknown;
openclaw?: {
install?: {
npmSpec?: unknown;
};
channel?: {
id?: unknown;
};
};
};
type BundledPluginRecord = {
dirName: string;
packageName: string;
manifestId: string;
installNpmSpec?: string;
channelId?: string;
};
const EXTENSIONS_ROOT = path.resolve(process.cwd(), "extensions");
const DIR_ID_EXCEPTIONS = new Map<string, string>([
// Historical directory name kept until a wider repo cleanup is worth the churn.
["kimi-coding", "kimi"],
]);
const ALLOWED_PACKAGE_SUFFIXES = ["", "-provider", "-plugin", "-speech", "-sandbox"] as const;
function readJsonFile<T>(filePath: string): T {
return JSON.parse(fs.readFileSync(filePath, "utf8")) as T;
}
function normalizeText(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
function readBundledPluginRecords(): BundledPluginRecord[] {
const records: BundledPluginRecord[] = [];
for (const dirName of fs.readdirSync(EXTENSIONS_ROOT).toSorted()) {
const rootDir = path.join(EXTENSIONS_ROOT, dirName);
const packagePath = path.join(rootDir, "package.json");
const manifestPath = path.join(rootDir, "openclaw.plugin.json");
if (!fs.existsSync(packagePath) || !fs.existsSync(manifestPath)) {
continue;
}
const manifest = readJsonFile<PluginManifestShape>(manifestPath);
const pkg = readJsonFile<OpenClawPackageShape>(packagePath);
const manifestId = normalizeText(manifest.id);
const packageName = normalizeText(pkg.name);
if (!manifestId || !packageName) {
continue;
}
records.push({
dirName,
packageName,
manifestId,
installNpmSpec: normalizeText(pkg.openclaw?.install?.npmSpec),
channelId: normalizeText(pkg.openclaw?.channel?.id),
});
}
return records;
}
function resolveAllowedPackageNamesForId(pluginId: string): string[] {
return ALLOWED_PACKAGE_SUFFIXES.map((suffix) => `@openclaw/${pluginId}${suffix}`);
}
describe("bundled plugin naming guardrails", () => {
it("keeps bundled workspace package names anchored to the plugin id", () => {
const mismatches = readBundledPluginRecords()
.filter(
({ packageName, manifestId }) =>
!resolveAllowedPackageNamesForId(manifestId).includes(packageName),
)
.map(
({ dirName, packageName, manifestId }) => `${dirName}: ${packageName} (id=${manifestId})`,
);
expect(
mismatches,
`Bundled extension package names must stay anchored to the manifest id via @openclaw/<id> or an approved suffix (${ALLOWED_PACKAGE_SUFFIXES.join(", ")}). Update the plugin naming docs and this invariant before adding a new naming form.\nFound: ${mismatches.join(", ") || "<none>"}`,
).toEqual([]);
});
it("keeps bundled workspace directories aligned with the plugin id unless explicitly allowlisted", () => {
const mismatches = readBundledPluginRecords()
.filter(
({ dirName, manifestId }) => (DIR_ID_EXCEPTIONS.get(dirName) ?? dirName) !== manifestId,
)
.map(({ dirName, manifestId }) => `${dirName} -> ${manifestId}`);
expect(
mismatches,
`Bundled extension directory names should match openclaw.plugin.json:id. If a legacy exception is unavoidable, add it to DIR_ID_EXCEPTIONS with a comment.\nFound: ${mismatches.join(", ") || "<none>"}`,
).toEqual([]);
});
it("keeps bundled openclaw.install.npmSpec aligned with the package name", () => {
const mismatches = readBundledPluginRecords()
.filter(
({ installNpmSpec, packageName }) =>
typeof installNpmSpec === "string" && installNpmSpec !== packageName,
)
.map(
({ dirName, packageName, installNpmSpec }) =>
`${dirName}: package=${packageName}, npmSpec=${installNpmSpec}`,
);
expect(
mismatches,
`Bundled openclaw.install.npmSpec values must match the package name so install/update paths stay deterministic.\nFound: ${mismatches.join(", ") || "<none>"}`,
).toEqual([]);
});
it("keeps bundled channel ids aligned with the canonical plugin id", () => {
const mismatches = readBundledPluginRecords()
.filter(
({ channelId, manifestId }) => typeof channelId === "string" && channelId !== manifestId,
)
.map(
({ dirName, manifestId, channelId }) =>
`${dirName}: channel=${channelId}, id=${manifestId}`,
);
expect(
mismatches,
`Bundled openclaw.channel.id values must match openclaw.plugin.json:id for the owning plugin.\nFound: ${mismatches.join(", ") || "<none>"}`,
).toEqual([]);
});
});