import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { listChannelPluginCatalogEntries } from "../../../src/channels/plugins/catalog.js"; function createCatalogEntry(params: { packageName: string; channelId: string; label: string; blurb: string; order?: number; }) { return { name: params.packageName, openclaw: { channel: { id: params.channelId, label: params.label, selectionLabel: params.label, docsPath: `/channels/${params.channelId}`, blurb: params.blurb, ...(params.order === undefined ? {} : { order: params.order }), }, install: { npmSpec: params.packageName, }, }, }; } function writeCatalogFile(catalogPath: string, entry: Record) { fs.writeFileSync( catalogPath, JSON.stringify({ entries: [entry], }), ); } function writeDiscoveredChannelPlugin(params: { stateDir: string; packageName: string; channelLabel: string; pluginId: string; blurb: string; }) { const pluginDir = path.join(params.stateDir, "extensions", "demo-channel-plugin"); fs.mkdirSync(pluginDir, { recursive: true }); fs.writeFileSync( path.join(pluginDir, "package.json"), JSON.stringify({ name: params.packageName, openclaw: { extensions: ["./index.js"], channel: { id: "demo-channel", label: params.channelLabel, selectionLabel: params.channelLabel, docsPath: "/channels/demo-channel", blurb: params.blurb, }, install: { npmSpec: params.packageName, }, }, }), "utf8", ); fs.writeFileSync( path.join(pluginDir, "openclaw.plugin.json"), JSON.stringify({ id: params.pluginId, configSchema: {}, }), "utf8", ); fs.writeFileSync(path.join(pluginDir, "index.js"), "module.exports = {}", "utf8"); } function expectCatalogIdsContain(params: { expectedId: string; catalogPaths?: string[]; env?: NodeJS.ProcessEnv; }) { const ids = listChannelPluginCatalogEntries({ ...(params.catalogPaths ? { catalogPaths: params.catalogPaths } : {}), ...(params.env ? { env: params.env } : {}), }).map((entry) => entry.id); expect(ids).toContain(params.expectedId); } function findCatalogEntry(params: { channelId: string; catalogPaths?: string[]; env?: NodeJS.ProcessEnv; }) { return listChannelPluginCatalogEntries({ ...(params.catalogPaths ? { catalogPaths: params.catalogPaths } : {}), ...(params.env ? { env: params.env } : {}), }).find((entry) => entry.id === params.channelId); } function expectCatalogEntryMatch(params: { channelId: string; expected: Record; catalogPaths?: string[]; env?: NodeJS.ProcessEnv; }) { expect( findCatalogEntry({ channelId: params.channelId, ...(params.catalogPaths ? { catalogPaths: params.catalogPaths } : {}), ...(params.env ? { env: params.env } : {}), }), ).toMatchObject(params.expected); } export function describeChannelPluginCatalogEntriesContract() { describe("channel plugin catalog entries contract", () => { it.each([ { name: "includes external catalog entries", setup: () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-catalog-")); const catalogPath = path.join(dir, "catalog.json"); writeCatalogFile( catalogPath, createCatalogEntry({ packageName: "@openclaw/demo-channel", channelId: "demo-channel", label: "Demo Channel", blurb: "Demo entry", order: 999, }), ); return { channelId: "demo-channel", catalogPaths: [catalogPath], expected: { id: "demo-channel" }, }; }, }, { name: "preserves plugin ids when they differ from channel ids", setup: () => { const stateDir = fs.mkdtempSync( path.join(os.tmpdir(), "openclaw-channel-catalog-state-"), ); writeDiscoveredChannelPlugin({ stateDir, packageName: "@vendor/demo-channel-plugin", channelLabel: "Demo Channel", pluginId: "@vendor/demo-runtime", blurb: "Demo channel", }); return { channelId: "demo-channel", env: { ...process.env, OPENCLAW_STATE_DIR: stateDir, OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins", }, expected: { pluginId: "@vendor/demo-runtime" }, }; }, }, { name: "keeps discovered plugins ahead of external catalog overrides", setup: () => { const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-catalog-state-")); const catalogPath = path.join(stateDir, "catalog.json"); writeDiscoveredChannelPlugin({ stateDir, packageName: "@vendor/demo-channel-plugin", channelLabel: "Demo Channel Runtime", pluginId: "@vendor/demo-channel-runtime", blurb: "discovered plugin", }); writeCatalogFile( catalogPath, createCatalogEntry({ packageName: "@vendor/demo-channel-catalog", channelId: "demo-channel", label: "Demo Channel Catalog", blurb: "external catalog", }), ); return { channelId: "demo-channel", catalogPaths: [catalogPath], env: { ...process.env, OPENCLAW_STATE_DIR: stateDir, CLAWDBOT_STATE_DIR: undefined, OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins", }, expected: { install: { npmSpec: "@vendor/demo-channel-plugin" }, meta: { label: "Demo Channel Runtime" }, pluginId: "@vendor/demo-channel-runtime", }, }; }, }, ] as const)("$name", ({ setup }) => { const setupResult = setup(); const { channelId, expected } = setupResult; expectCatalogEntryMatch({ channelId, expected, ...("catalogPaths" in setupResult ? { catalogPaths: setupResult.catalogPaths } : {}), ...("env" in setupResult ? { env: setupResult.env } : {}), }); }); }); } export function describeChannelPluginCatalogPathResolutionContract() { describe("channel plugin catalog path resolution contract", () => { it.each([ { name: "uses the provided env for external catalog path resolution", setup: () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-catalog-home-")); const catalogPath = path.join(home, "catalog.json"); writeCatalogFile( catalogPath, createCatalogEntry({ packageName: "@openclaw/env-demo-channel", channelId: "env-demo-channel", label: "Env Demo Channel", blurb: "Env demo entry", order: 1000, }), ); return { env: { ...process.env, OPENCLAW_PLUGIN_CATALOG_PATHS: "~/catalog.json", OPENCLAW_HOME: home, HOME: home, }, expectedId: "env-demo-channel", }; }, }, { name: "uses the provided env for default catalog paths", setup: () => { const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-catalog-state-")); const catalogPath = path.join(stateDir, "plugins", "catalog.json"); fs.mkdirSync(path.dirname(catalogPath), { recursive: true }); writeCatalogFile( catalogPath, createCatalogEntry({ packageName: "@openclaw/default-env-demo", channelId: "default-env-demo", label: "Default Env Demo", blurb: "Default env demo entry", }), ); return { env: { ...process.env, OPENCLAW_STATE_DIR: stateDir, }, expectedId: "default-env-demo", }; }, }, ] as const)("$name", ({ setup }) => { const { env, expectedId } = setup(); expectCatalogIdsContain({ env, expectedId }); }); }); }