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 | 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"]); }); });