openclaw/src/plugins/bundled-plugin-metadata.tes...

286 lines
9.0 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
collectBundledPluginMetadata,
writeBundledPluginMetadataModule,
} from "../../scripts/generate-bundled-plugin-metadata.mjs";
import {
BUNDLED_PLUGIN_METADATA,
resolveBundledPluginGeneratedPath,
} from "./bundled-plugin-metadata.js";
import {
createGeneratedPluginTempRoot,
installGeneratedPluginTempRootCleanup,
pluginTestRepoRoot as repoRoot,
writeJson,
} from "./generated-plugin-test-helpers.js";
const BUNDLED_PLUGIN_METADATA_TEST_TIMEOUT_MS = 300_000;
installGeneratedPluginTempRootCleanup();
function expectTestOnlyArtifactsExcluded(artifacts: readonly string[]) {
artifacts.forEach((artifact) => {
expect(artifact).not.toMatch(/^test-/);
expect(artifact).not.toContain(".test-");
expect(artifact).not.toMatch(/\.test\.js$/);
});
}
function expectGeneratedPathResolution(tempRoot: string, expectedRelativePath: string) {
expect(
resolveBundledPluginGeneratedPath(tempRoot, {
source: "plugin/index.ts",
built: "plugin/index.js",
}),
).toBe(path.join(tempRoot, expectedRelativePath));
}
function expectArtifactPresence(
artifacts: readonly string[] | undefined,
params: { contains?: readonly string[]; excludes?: readonly string[] },
) {
if (params.contains) {
for (const artifact of params.contains) {
expect(artifacts).toContain(artifact);
}
}
if (params.excludes) {
for (const artifact of params.excludes) {
expect(artifacts).not.toContain(artifact);
}
}
}
async function writeGeneratedMetadataModule(params: {
repoRoot: string;
outputPath?: string;
check?: boolean;
}) {
return writeBundledPluginMetadataModule({
repoRoot: params.repoRoot,
outputPath: params.outputPath ?? "src/plugins/bundled-plugin-metadata.generated.ts",
...(params.check ? { check: true } : {}),
});
}
async function expectGeneratedMetadataModuleState(params: {
repoRoot: string;
check?: boolean;
expected: { changed?: boolean; wrote?: boolean };
}) {
const result = await writeGeneratedMetadataModule({
repoRoot: params.repoRoot,
...(params.check ? { check: true } : {}),
});
expect(result).toEqual(expect.objectContaining(params.expected));
return result;
}
describe("bundled plugin metadata", () => {
it(
"matches the generated metadata snapshot",
{ timeout: BUNDLED_PLUGIN_METADATA_TEST_TIMEOUT_MS },
async () => {
await expect(collectBundledPluginMetadata({ repoRoot })).resolves.toEqual(
BUNDLED_PLUGIN_METADATA,
);
},
);
it("captures setup-entry metadata for bundled channel plugins", () => {
const discord = BUNDLED_PLUGIN_METADATA.find((entry) => entry.dirName === "discord");
expect(discord?.source).toEqual({ source: "./index.ts", built: "index.js" });
expect(discord?.setupSource).toEqual({ source: "./setup-entry.ts", built: "setup-entry.js" });
expectArtifactPresence(discord?.publicSurfaceArtifacts, {
contains: ["api.js", "runtime-api.js", "session-key-api.js"],
excludes: ["test-api.js"],
});
expectArtifactPresence(discord?.runtimeSidecarArtifacts, {
contains: ["runtime-api.js"],
});
expect(discord?.manifest.id).toBe("discord");
expect(discord?.manifest.channelConfigs?.discord).toEqual(
expect.objectContaining({
schema: expect.objectContaining({ type: "object" }),
}),
);
});
it("excludes test-only public surface artifacts", () => {
BUNDLED_PLUGIN_METADATA.forEach((entry) =>
expectTestOnlyArtifactsExcluded(entry.publicSurfaceArtifacts ?? []),
);
});
it("prefers built generated paths when present and falls back to source paths", () => {
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-metadata-");
fs.mkdirSync(path.join(tempRoot, "plugin"), { recursive: true });
fs.writeFileSync(path.join(tempRoot, "plugin", "index.ts"), "export {};\n", "utf8");
expectGeneratedPathResolution(tempRoot, path.join("plugin", "index.ts"));
fs.writeFileSync(path.join(tempRoot, "plugin", "index.js"), "export {};\n", "utf8");
expectGeneratedPathResolution(tempRoot, path.join("plugin", "index.js"));
});
it("supports check mode for stale generated artifacts", async () => {
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-generated-");
writeJson(path.join(tempRoot, "extensions", "alpha", "package.json"), {
name: "@openclaw/alpha",
version: "0.0.1",
openclaw: {
extensions: ["./index.ts"],
},
});
writeJson(path.join(tempRoot, "extensions", "alpha", "openclaw.plugin.json"), {
id: "alpha",
configSchema: { type: "object" },
});
await expectGeneratedMetadataModuleState({
repoRoot: tempRoot,
expected: { wrote: true },
});
await expectGeneratedMetadataModuleState({
repoRoot: tempRoot,
check: true,
expected: { changed: false, wrote: false },
});
fs.writeFileSync(
path.join(tempRoot, "src/plugins/bundled-plugin-metadata.generated.ts"),
"// stale\n",
"utf8",
);
await expectGeneratedMetadataModuleState({
repoRoot: tempRoot,
check: true,
expected: { changed: true, wrote: false },
});
});
it("merges generated channel schema metadata with manifest-owned channel config fields", async () => {
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-channel-configs-");
writeJson(path.join(tempRoot, "extensions", "alpha", "package.json"), {
name: "@openclaw/alpha",
version: "0.0.1",
openclaw: {
extensions: ["./index.ts"],
channel: {
id: "alpha",
label: "Alpha Root Label",
blurb: "Alpha Root Description",
preferOver: ["alpha-legacy"],
},
},
});
writeJson(path.join(tempRoot, "extensions", "alpha", "openclaw.plugin.json"), {
id: "alpha",
channels: ["alpha"],
configSchema: { type: "object" },
channelConfigs: {
alpha: {
schema: { type: "object", properties: { stale: { type: "boolean" } } },
label: "Manifest Label",
uiHints: {
"channels.alpha.explicitOnly": {
help: "manifest hint",
},
},
},
},
});
fs.writeFileSync(
path.join(tempRoot, "extensions", "alpha", "index.ts"),
"export {};\n",
"utf8",
);
fs.mkdirSync(path.join(tempRoot, "extensions", "alpha", "src"), { recursive: true });
fs.writeFileSync(
path.join(tempRoot, "extensions", "alpha", "src", "config-schema.js"),
[
"export const AlphaChannelConfigSchema = {",
" schema: {",
" type: 'object',",
" properties: { generated: { type: 'string' } },",
" },",
" uiHints: {",
" 'channels.alpha.generatedOnly': { help: 'generated hint' },",
" },",
"};",
"",
].join("\n"),
"utf8",
);
const entries = await collectBundledPluginMetadata({ repoRoot: tempRoot });
const channelConfigs = entries[0]?.manifest.channelConfigs as
| Record<string, unknown>
| undefined;
expect(channelConfigs?.alpha).toEqual({
schema: {
type: "object",
properties: {
generated: { type: "string" },
},
},
label: "Manifest Label",
description: "Alpha Root Description",
preferOver: ["alpha-legacy"],
uiHints: {
"channels.alpha.generatedOnly": { help: "generated hint" },
"channels.alpha.explicitOnly": { help: "manifest hint" },
},
});
});
it("captures top-level public surface artifacts without duplicating the primary entrypoints", async () => {
const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-public-artifacts-");
writeJson(path.join(tempRoot, "extensions", "alpha", "package.json"), {
name: "@openclaw/alpha",
version: "0.0.1",
openclaw: {
extensions: ["./index.ts"],
setupEntry: "./setup-entry.ts",
},
});
writeJson(path.join(tempRoot, "extensions", "alpha", "openclaw.plugin.json"), {
id: "alpha",
configSchema: { type: "object" },
});
fs.writeFileSync(
path.join(tempRoot, "extensions", "alpha", "index.ts"),
"export {};\n",
"utf8",
);
fs.writeFileSync(
path.join(tempRoot, "extensions", "alpha", "setup-entry.ts"),
"export {};\n",
"utf8",
);
fs.writeFileSync(path.join(tempRoot, "extensions", "alpha", "api.ts"), "export {};\n", "utf8");
fs.writeFileSync(
path.join(tempRoot, "extensions", "alpha", "runtime-api.ts"),
"export {};\n",
"utf8",
);
const entries = await collectBundledPluginMetadata({ repoRoot: tempRoot });
const firstEntry = entries[0] as
| {
publicSurfaceArtifacts?: string[];
runtimeSidecarArtifacts?: string[];
}
| undefined;
expect(firstEntry?.publicSurfaceArtifacts).toEqual(["api.js", "runtime-api.js"]);
expect(firstEntry?.runtimeSidecarArtifacts).toEqual(["runtime-api.js"]);
});
});