import fs from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { clearBundledPluginMetadataCache, listBundledPluginMetadata, resolveBundledPluginGeneratedPath, resolveBundledPluginRepoEntryPath, } from "./bundled-plugin-metadata.js"; import { createGeneratedPluginTempRoot, installGeneratedPluginTempRootCleanup, pluginTestRepoRoot as repoRoot, writeJson, } from "./generated-plugin-test-helpers.js"; import { collectBundledRuntimeSidecarPaths } from "./runtime-sidecar-paths-baseline.js"; import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "./runtime-sidecar-paths.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); } } } describe("bundled plugin metadata", () => { it( "matches the runtime metadata snapshot", { timeout: BUNDLED_PLUGIN_METADATA_TEST_TIMEOUT_MS }, () => { expect(listBundledPluginMetadata({ rootDir: repoRoot })).toEqual(listBundledPluginMetadata()); }, ); it( "matches the checked-in runtime sidecar path baseline", { timeout: BUNDLED_PLUGIN_METADATA_TEST_TIMEOUT_MS }, () => { expect(BUNDLED_RUNTIME_SIDECAR_PATHS).toEqual( collectBundledRuntimeSidecarPaths({ rootDir: repoRoot }), ); }, ); it("captures setup-entry metadata for bundled channel plugins", () => { const discord = listBundledPluginMetadata().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("loads tlon channel config metadata from the lightweight schema surface", () => { const tlon = listBundledPluginMetadata().find((entry) => entry.dirName === "tlon"); expect(tlon?.manifest.channelConfigs?.tlon).toEqual( expect.objectContaining({ schema: expect.objectContaining({ type: "object" }), }), ); }); it("keeps bundled persisted-auth metadata on channel package manifests", () => { const whatsapp = listBundledPluginMetadata().find((entry) => entry.dirName === "whatsapp"); expect(whatsapp?.packageManifest?.channel?.persistedAuthState).toEqual({ specifier: "./auth-presence", exportName: "hasAnyWhatsAppAuth", }); const matrix = listBundledPluginMetadata().find((entry) => entry.dirName === "matrix"); expect(matrix?.packageManifest?.channel?.persistedAuthState).toEqual({ specifier: "./auth-presence", exportName: "hasAnyMatrixAuth", }); }); it("keeps bundled configured-state metadata on channel package manifests", () => { const configuredChannels = listBundledPluginMetadata() .filter((entry) => ["discord", "irc", "slack", "telegram"].includes(entry.dirName)) .map((entry) => ({ dir: entry.dirName, configuredState: entry.packageManifest?.channel?.configuredState, })); expect(configuredChannels).toEqual([ { dir: "discord", configuredState: { specifier: "./configured-state", exportName: "hasDiscordConfiguredState", }, }, { dir: "irc", configuredState: { specifier: "./configured-state", exportName: "hasIrcConfiguredState", }, }, { dir: "slack", configuredState: { specifier: "./configured-state", exportName: "hasSlackConfiguredState", }, }, { dir: "telegram", configuredState: { specifier: "./configured-state", exportName: "hasTelegramConfiguredState", }, }, ]); }); it("excludes test-only public surface artifacts", () => { listBundledPluginMetadata().forEach((entry) => expectTestOnlyArtifactsExcluded(entry.publicSurfaceArtifacts ?? []), ); }); it("keeps config schemas on all bundled plugin manifests", () => { for (const entry of listBundledPluginMetadata()) { expect(entry.manifest.configSchema).toEqual(expect.any(Object)); } }); 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("resolves bundled repo entry paths from dist before workspace source", () => { const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-repo-entry-"); const pluginRoot = path.join(tempRoot, "extensions", "alpha"); const distPluginRoot = path.join(tempRoot, "dist", "extensions", "alpha"); writeJson(path.join(pluginRoot, "package.json"), { name: "@openclaw/alpha", version: "0.0.1", openclaw: { extensions: ["./index.ts"], }, }); writeJson(path.join(pluginRoot, "openclaw.plugin.json"), { id: "alpha", configSchema: { type: "object" }, }); fs.writeFileSync(path.join(pluginRoot, "index.ts"), "export const source = true;\n", "utf8"); expect( resolveBundledPluginRepoEntryPath({ rootDir: tempRoot, pluginId: "alpha", preferBuilt: true, }), ).toBe(path.join(pluginRoot, "index.ts")); fs.mkdirSync(distPluginRoot, { recursive: true }); fs.writeFileSync(path.join(distPluginRoot, "index.js"), "export const built = true;\n", "utf8"); clearBundledPluginMetadataCache(); expect( resolveBundledPluginRepoEntryPath({ rootDir: tempRoot, pluginId: "alpha", preferBuilt: true, }), ).toBe(path.join(distPluginRoot, "index.js")); }); it("merges runtime channel schema metadata with manifest-owned channel config fields", () => { 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", ); clearBundledPluginMetadataCache(); const entries = listBundledPluginMetadata({ rootDir: 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", () => { 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", ); clearBundledPluginMetadataCache(); const entries = listBundledPluginMetadata({ rootDir: 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"]); }); it("loads channel config metadata from built public surfaces in dist-only roots", () => { const tempRoot = createGeneratedPluginTempRoot("openclaw-bundled-plugin-dist-config-"); const distRoot = path.join(tempRoot, "dist"); writeJson(path.join(distRoot, "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", }, }, }); writeJson(path.join(distRoot, "extensions", "alpha", "openclaw.plugin.json"), { id: "alpha", configSchema: { type: "object", properties: {}, }, channels: ["alpha"], channelConfigs: { alpha: { schema: { type: "object", properties: { stale: { type: "boolean" } } }, uiHints: { "channels.alpha.explicitOnly": { help: "manifest hint", }, }, }, }, }); fs.writeFileSync( path.join(distRoot, "extensions", "alpha", "index.js"), "export {};\n", "utf8", ); fs.writeFileSync( path.join(distRoot, "extensions", "alpha", "channel-config-api.js"), [ "export const AlphaChannelConfigSchema = {", " schema: {", " type: 'object',", " properties: { built: { type: 'string' } },", " },", " uiHints: {", " 'channels.alpha.generatedOnly': { help: 'built hint' },", " },", "};", "", ].join("\n"), "utf8", ); clearBundledPluginMetadataCache(); const entries = listBundledPluginMetadata({ rootDir: distRoot }); const channelConfigs = entries[0]?.manifest.channelConfigs as | Record | undefined; expect(channelConfigs?.alpha).toEqual({ schema: { type: "object", properties: { built: { type: "string" }, }, }, label: "Alpha Root Label", description: "Alpha Root Description", uiHints: { "channels.alpha.generatedOnly": { help: "built hint" }, "channels.alpha.explicitOnly": { help: "manifest hint" }, }, }); }); });