Files
openclaw/test/plugin-npm-package-manifest.test.ts
2026-05-02 23:47:25 -07:00

175 lines
6.0 KiB
TypeScript

import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
resolveAugmentedPluginNpmPackageJson,
resolveAugmentedPluginNpmManifest,
withAugmentedPluginNpmManifestForPackage,
} from "../scripts/lib/plugin-npm-package-manifest.mjs";
import { cleanupTempDirs, makeTempRepoRoot, writeJsonFile } from "./helpers/temp-repo.js";
const tempDirs: string[] = [];
afterEach(() => {
cleanupTempDirs(tempDirs);
});
function writeGeneratedChannelMetadata(repoDir: string): void {
const metadataPath = join(
repoDir,
"src",
"config",
"bundled-channel-config-metadata.generated.ts",
);
mkdirSync(join(repoDir, "src", "config"), { recursive: true });
writeFileText(
metadataPath,
`export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
{
pluginId: "twitch",
channelId: "twitch",
label: "Twitch",
description: "Twitch chat integration",
schema: {
type: "object",
required: ["channelName"],
properties: {
channelName: { type: "string" },
},
},
},
] as const;
`,
);
}
function writeFileText(filePath: string, text: string): void {
mkdirSync(dirname(filePath), { recursive: true });
// writeJsonFile intentionally owns JSON formatting only.
writeFileSync(filePath, text, "utf8");
}
function writePublishablePluginPackage(repoDir: string): string {
const packageDir = join(repoDir, "extensions", "diffs");
mkdirSync(packageDir, { recursive: true });
writeJsonFile(join(packageDir, "package.json"), {
name: "@openclaw/diffs",
version: "2026.5.3",
type: "module",
openclaw: {
extensions: ["./index.ts"],
setupEntry: "./setup-entry.ts",
compat: {
pluginApi: ">=2026.4.30",
},
release: {
publishToNpm: true,
},
},
});
writeJsonFile(join(packageDir, "openclaw.plugin.json"), { id: "diffs" });
writeFileText(join(packageDir, "README.md"), "# Diffs\n");
writeFileText(join(packageDir, "SKILL.md"), "# Diffs Skill\n");
writeFileText(join(packageDir, "skills", "diffs", "SKILL.md"), "# Diffs Skill\n");
return packageDir;
}
describe("plugin npm package manifest staging", () => {
it("overlays generated channel configs while packing and restores source manifest", () => {
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-package-manifest-");
const packageDir = join(repoDir, "extensions", "twitch");
mkdirSync(packageDir, { recursive: true });
const sourceManifest = {
id: "twitch",
channels: ["twitch"],
configSchema: {
type: "object",
additionalProperties: false,
properties: {},
},
};
writeJsonFile(join(packageDir, "openclaw.plugin.json"), sourceManifest);
writeGeneratedChannelMetadata(repoDir);
const resolved = resolveAugmentedPluginNpmManifest({
repoRoot: repoDir,
packageDir,
});
expect(resolved.changed).toBe(true);
expect(resolved.manifest).toMatchObject({
channelConfigs: {
twitch: {
label: "Twitch",
schema: {
required: ["channelName"],
},
},
},
});
const originalText = readFileSync(join(packageDir, "openclaw.plugin.json"), "utf8");
withAugmentedPluginNpmManifestForPackage({ repoRoot: repoDir, packageDir }, () => {
const stagedManifest = JSON.parse(
readFileSync(join(packageDir, "openclaw.plugin.json"), "utf8"),
);
expect(stagedManifest.channelConfigs.twitch.description).toBe("Twitch chat integration");
});
expect(readFileSync(join(packageDir, "openclaw.plugin.json"), "utf8")).toBe(originalText);
});
it("overlays package-local runtime metadata while packing and restores source package json", () => {
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-package-runtime-");
const packageDir = writePublishablePluginPackage(repoDir);
writeFileText(join(packageDir, "dist", "index.js"), "export {};\n");
writeFileText(join(packageDir, "dist", "setup-entry.js"), "export {};\n");
const resolved = resolveAugmentedPluginNpmPackageJson({
repoRoot: repoDir,
packageDir,
});
expect(resolved.changed).toBe(true);
expect(resolved.packageJson).toMatchObject({
files: ["dist/**", "openclaw.plugin.json", "README.md", "SKILL.md", "skills/**"],
peerDependencies: {
openclaw: ">=2026.4.30",
},
peerDependenciesMeta: {
openclaw: {
optional: true,
},
},
openclaw: {
runtimeExtensions: ["./dist/index.js"],
runtimeSetupEntry: "./dist/setup-entry.js",
},
});
const originalText = readFileSync(join(packageDir, "package.json"), "utf8");
withAugmentedPluginNpmManifestForPackage({ repoRoot: repoDir, packageDir }, () => {
const stagedPackageJson = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
expect(stagedPackageJson.openclaw.extensions).toEqual(["./index.ts"]);
expect(stagedPackageJson.openclaw.runtimeExtensions).toEqual(["./dist/index.js"]);
expect(stagedPackageJson.openclaw.runtimeSetupEntry).toBe("./dist/setup-entry.js");
expect(stagedPackageJson.files).toContain("dist/**");
expect(stagedPackageJson.files).toContain("skills/**");
expect(stagedPackageJson.peerDependencies.openclaw).toBe(">=2026.4.30");
expect(stagedPackageJson.peerDependenciesMeta.openclaw.optional).toBe(true);
});
expect(readFileSync(join(packageDir, "package.json"), "utf8")).toBe(originalText);
});
it("refuses to pack publishable plugins before package-local runtime files exist", () => {
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-package-runtime-missing-");
const packageDir = writePublishablePluginPackage(repoDir);
expect(() =>
resolveAugmentedPluginNpmPackageJson({
repoRoot: repoDir,
packageDir,
}),
).toThrow(
"package-local plugin runtime is missing for diffs: ./dist/index.js, ./dist/setup-entry.js",
);
});
});