Files
openclaw/src/plugins/setup-registry.test.ts
2026-04-07 09:52:56 +01:00

223 lines
6.9 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
const tempDirs: string[] = [];
const mocks = vi.hoisted(() => ({
createJiti: vi.fn(),
discoverOpenClawPlugins: vi.fn(),
loadPluginManifestRegistry: vi.fn(),
}));
vi.mock("jiti", () => ({
createJiti: (...args: Parameters<typeof mocks.createJiti>) => mocks.createJiti(...args),
}));
vi.mock("./discovery.js", () => ({
discoverOpenClawPlugins: (...args: Parameters<typeof mocks.discoverOpenClawPlugins>) =>
mocks.discoverOpenClawPlugins(...args),
}));
vi.mock("./manifest-registry.js", () => ({
loadPluginManifestRegistry: (...args: Parameters<typeof mocks.loadPluginManifestRegistry>) =>
mocks.loadPluginManifestRegistry(...args),
}));
let clearPluginSetupRegistryCache: typeof import("./setup-registry.js").clearPluginSetupRegistryCache;
let resolvePluginSetupRegistry: typeof import("./setup-registry.js").resolvePluginSetupRegistry;
let runPluginSetupConfigMigrations: typeof import("./setup-registry.js").runPluginSetupConfigMigrations;
function makeTempDir(): string {
return makeTrackedTempDir("openclaw-setup-registry", tempDirs);
}
afterEach(() => {
cleanupTrackedTempDirs(tempDirs);
});
describe("setup-registry getJiti", () => {
beforeEach(async () => {
vi.resetModules();
({ clearPluginSetupRegistryCache, resolvePluginSetupRegistry, runPluginSetupConfigMigrations } =
await import("./setup-registry.js"));
clearPluginSetupRegistryCache();
mocks.createJiti.mockReset();
mocks.discoverOpenClawPlugins.mockReset();
mocks.loadPluginManifestRegistry.mockReset();
mocks.discoverOpenClawPlugins.mockReturnValue({
candidates: [],
diagnostics: [],
});
mocks.createJiti.mockImplementation(
(_modulePath: string, _options?: Record<string, unknown>) => {
return () => ({ default: {} });
},
);
});
it("disables native jiti loading on Windows for setup-api modules", () => {
const pluginRoot = makeTempDir();
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
mocks.loadPluginManifestRegistry.mockReturnValue({
plugins: [{ id: "test-plugin", rootDir: pluginRoot }],
diagnostics: [],
});
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
try {
resolvePluginSetupRegistry({
workspaceDir: pluginRoot,
env: {},
});
} finally {
platformSpy.mockRestore();
}
expect(mocks.createJiti).toHaveBeenCalledTimes(1);
expect(mocks.createJiti.mock.calls[0]?.[0]).toBe(path.join(pluginRoot, "setup-api.js"));
expect(mocks.createJiti.mock.calls[0]?.[1]).toEqual(
expect.objectContaining({
tryNative: false,
}),
);
});
it("skips setup-api loading when config has no relevant migration triggers", () => {
const pluginRoot = makeTempDir();
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
mocks.loadPluginManifestRegistry.mockReturnValue({
plugins: [
{
id: "amazon-bedrock",
rootDir: pluginRoot,
configContracts: {
compatibilityMigrationPaths: ["models.bedrockDiscovery"],
},
},
],
diagnostics: [],
});
mocks.createJiti.mockImplementation(() => {
return () => ({
default: {
register(api: {
registerConfigMigration: (migrate: (config: unknown) => unknown) => void;
}) {
api.registerConfigMigration((config) => ({ config, changes: ["unexpected"] }));
},
},
});
});
const result = runPluginSetupConfigMigrations({
config: {
models: {
providers: {
openai: { baseUrl: "https://api.openai.com/v1" },
},
},
} as never,
env: {},
});
expect(result.changes).toEqual([]);
expect(mocks.createJiti).not.toHaveBeenCalled();
});
it("loads only plugins whose manifest migration triggers match the config", () => {
const bedrockRoot = makeTempDir();
const voiceCallRoot = makeTempDir();
fs.writeFileSync(path.join(bedrockRoot, "setup-api.js"), "export default {};\n", "utf-8");
fs.writeFileSync(path.join(voiceCallRoot, "setup-api.js"), "export default {};\n", "utf-8");
mocks.loadPluginManifestRegistry.mockReturnValue({
plugins: [
{
id: "amazon-bedrock",
rootDir: bedrockRoot,
configContracts: {
compatibilityMigrationPaths: ["models.bedrockDiscovery"],
},
},
{
id: "voice-call",
rootDir: voiceCallRoot,
configContracts: {
compatibilityMigrationPaths: ["plugins.entries.voice-call.config"],
},
},
],
diagnostics: [],
});
mocks.createJiti.mockImplementation((modulePath: string) => {
const pluginId = modulePath.includes(bedrockRoot) ? "amazon-bedrock" : "voice-call";
return () => ({
default: {
register(api: {
registerConfigMigration: (migrate: (config: unknown) => unknown) => void;
}) {
api.registerConfigMigration((config) => ({
config,
changes: [pluginId],
}));
},
},
});
});
const result = runPluginSetupConfigMigrations({
config: {
models: {
bedrockDiscovery: {
enabled: true,
},
},
} as never,
env: {},
});
expect(result.changes).toEqual(["amazon-bedrock"]);
expect(mocks.createJiti).toHaveBeenCalledTimes(1);
expect(mocks.createJiti.mock.calls[0]?.[0]).toBe(path.join(bedrockRoot, "setup-api.js"));
});
it("still loads explicitly configured plugin entries without manifest trigger metadata", () => {
const pluginRoot = makeTempDir();
fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
mocks.loadPluginManifestRegistry.mockReturnValue({
plugins: [{ id: "voice-call", rootDir: pluginRoot }],
diagnostics: [],
});
mocks.createJiti.mockImplementation(() => {
return () => ({
default: {
register(api: {
registerConfigMigration: (migrate: (config: unknown) => unknown) => void;
}) {
api.registerConfigMigration((config) => ({ config, changes: ["voice-call"] }));
},
},
});
});
const result = runPluginSetupConfigMigrations({
config: {
plugins: {
entries: {
"voice-call": {
config: {
provider: "log",
},
},
},
},
} as never,
env: {},
});
expect(result.changes).toEqual(["voice-call"]);
expect(mocks.createJiti).toHaveBeenCalledTimes(1);
});
});