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) => mocks.createJiti(...args), })); vi.mock("./discovery.js", () => ({ discoverOpenClawPlugins: (...args: Parameters) => mocks.discoverOpenClawPlugins(...args), })); vi.mock("./manifest-registry.js", () => ({ loadPluginManifestRegistry: (...args: Parameters) => 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) => { 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); }); });