// Covers active runtime plugin registry state and reset behavior. import { mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; import { afterAll, afterEach, describe, expect, it } from "vitest"; import { getLoadedRuntimePluginRegistry, listLoadedRuntimePluginIds, listRuntimePluginIdsFromRegistry, createRuntimePluginManifestLookup, } from "./active-runtime-registry.js"; import { resolvePluginLoadCacheContext } from "./loader-load-context.js"; import { cleanupPluginLoaderFixturesForTest, clearPluginLoaderCache, EMPTY_PLUGIN_SCHEMA, loadOpenClawPlugins, makePluginLoaderTempDir, writePlugin, } from "./loader.test-fixtures.js"; import { createPluginMetadataSnapshotFixture } from "./plugin-metadata.test-support.js"; import { createEmptyPluginRegistry } from "./registry-empty.js"; import type { PluginRegistry } from "./registry-types.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "./runtime.js"; afterEach(() => { clearPluginLoaderCache(); resetPluginRuntimeStateForTest(); }); afterAll(cleanupPluginLoaderFixturesForTest); function createRegistryWithPlugin(pluginId: string): PluginRegistry { const registry = createEmptyPluginRegistry(); registry.plugins.push({ id: pluginId, status: "loaded", } as never); return registry; } function createOwnedRegistryWithPlugin(pluginId: string, rootDir: string): PluginRegistry { const registry = createEmptyPluginRegistry(); registry.plugins.push({ id: pluginId, origin: "bundled", rootDir, source: `${rootDir}/index.js`, status: "loaded", } as never); return registry; } describe("getLoadedRuntimePluginRegistry", () => { it("treats an explicit empty plugin scope as empty", () => { setActivePluginRegistry(createRegistryWithPlugin("stale"), "stale", "default", "/tmp/ws"); expect( getLoadedRuntimePluginRegistry({ workspaceDir: "/tmp/ws", requiredPluginIds: [], }), ).toBeUndefined(); const emptyRegistry = createEmptyPluginRegistry(); setActivePluginRegistry(emptyRegistry, "empty", "default", "/tmp/ws"); expect( getLoadedRuntimePluginRegistry({ workspaceDir: "/tmp/ws", requiredPluginIds: [], }), ).toBe(emptyRegistry); }); it("does not treat disabled plugin records as an empty plugin scope", () => { const disabledRegistry = createEmptyPluginRegistry(); disabledRegistry.plugins.push({ id: "disabled", status: "disabled", } as never); setActivePluginRegistry(disabledRegistry, "disabled", "default", "/tmp/ws"); expect( getLoadedRuntimePluginRegistry({ workspaceDir: "/tmp/ws", requiredPluginIds: [], }), ).toBeUndefined(); }); it("does not treat diagnostics as loaded plugin records", () => { const failedRegistry = createEmptyPluginRegistry(); failedRegistry.plugins.push({ id: "failed", status: "error", } as never); failedRegistry.diagnostics.push({ level: "error", pluginId: "failed", message: "failed to load", } as never); setActivePluginRegistry(failedRegistry, "failed", "default", "/tmp/ws"); expect( getLoadedRuntimePluginRegistry({ workspaceDir: "/tmp/ws", requiredPluginIds: ["failed"], }), ).toBeUndefined(); }); it("does not treat setup-only registrations as loaded plugin records", () => { const setupRegistry = createEmptyPluginRegistry(); setupRegistry.plugins.push({ id: "setup-only", status: "disabled", } as never); setupRegistry.channelSetups.push({ pluginId: "setup-only", } as never); setActivePluginRegistry(setupRegistry, "setup-only", "default", "/tmp/ws"); expect( getLoadedRuntimePluginRegistry({ workspaceDir: "/tmp/ws", requiredPluginIds: ["setup-only"], }), ).toBeUndefined(); }); it("does not treat deferred plugin metadata as a loaded runtime", () => { const deferredRegistry = createEmptyPluginRegistry(); deferredRegistry.plugins.push({ id: "deferred", format: "openclaw", imported: false, status: "loaded", } as never); setActivePluginRegistry(deferredRegistry, "deferred", "default", "/tmp/ws"); expect( getLoadedRuntimePluginRegistry({ workspaceDir: "/tmp/ws", requiredPluginIds: ["deferred"], }), ).toBeUndefined(); expect(listLoadedRuntimePluginIds()).not.toContain("deferred"); expect(listRuntimePluginIdsFromRegistry(deferredRegistry)).not.toContain("deferred"); }); it("accepts metadata-only bundle plugins as loaded runtimes", () => { const bundleRegistry = createEmptyPluginRegistry(); bundleRegistry.plugins.push({ id: "bundle", format: "bundle", imported: false, status: "loaded", } as never); setActivePluginRegistry(bundleRegistry, "bundle", "default", "/tmp/ws"); expect( getLoadedRuntimePluginRegistry({ workspaceDir: "/tmp/ws", requiredPluginIds: ["bundle"], }), ).toBe(bundleRegistry); expect(listLoadedRuntimePluginIds()).toContain("bundle"); expect(listRuntimePluginIdsFromRegistry(bundleRegistry)).toContain("bundle"); }); it("reuses scoped loaded owners when load options differ from the active registry", () => { const registry = createRegistryWithPlugin("demo"); setActivePluginRegistry(registry, "gateway-root-key", "default", "/tmp/ws"); expect( getLoadedRuntimePluginRegistry({ loadOptions: { workspaceDir: "/tmp/ws", onlyPluginIds: ["demo"] }, workspaceDir: "/tmp/ws", requiredPluginIds: ["demo"], }), ).toBe(registry); }); it("keeps exact-key semantics for unscoped load-option requests", () => { setActivePluginRegistry( createRegistryWithPlugin("demo"), "gateway-root-key", "default", "/tmp/ws", ); expect( getLoadedRuntimePluginRegistry({ loadOptions: { workspaceDir: "/tmp/ws" }, workspaceDir: "/tmp/ws", }), ).toBeUndefined(); }); it("does not reuse workspace-agnostic registries for workspace-specific requests", () => { setActivePluginRegistry(createRegistryWithPlugin("demo"), "demo"); expect( getLoadedRuntimePluginRegistry({ workspaceDir: "/tmp/ws", requiredPluginIds: ["demo"], }), ).toBeUndefined(); }); it("honors the requested workspace when scoped load options match the active key", () => { const registry = createRegistryWithPlugin("demo"); const loadOptions = { config: {}, installRecords: {}, workspaceDir: "/tmp/owner-workspace", onlyPluginIds: ["demo"], }; setActivePluginRegistry( registry, resolvePluginLoadCacheContext(loadOptions).cacheKey, "default", loadOptions.workspaceDir, ); expect( getLoadedRuntimePluginRegistry({ loadOptions, workspaceDir: "/tmp/request-workspace" }), ).toBeUndefined(); }); it("rejects a request registry when a workspace selects another physical owner", () => { const registry = createOwnedRegistryWithPlugin("demo", "/plugins/demo"); expect( createRuntimePluginManifestLookup(registry, [ { id: "demo", origin: "workspace", rootDir: "/tmp/session-workspace/.openclaw/extensions/demo", source: "/tmp/session-workspace/.openclaw/extensions/demo/index.js", } as never, ])("demo"), ).toBeUndefined(); }); }); it.each(["setup", "full"] as const)( "matches the executable setup source selected by a %s load", (channelPluginLoadIntent) => { const runtime = writePlugin({ id: "setup-fixture", filename: "index.cjs", body: 'module.exports = { id: "setup-fixture", register() {} };', }); for (const label of ["old", "new"]) { writeFileSync( path.join(runtime.dir, `${label}.cjs`), `module.exports = { plugin: { id: "setup-channel", meta: { id: "setup-channel", label: ${JSON.stringify(label)}, selectionLabel: "Fixture", docsPath: "/synthetic", blurb: "Synthetic fixture" }, capabilities: { chatTypes: ["direct"] }, config: { listAccountIds: () => [], resolveAccount: () => ({}) }, }};`, ); } const options = (label: string) => ({ config: { plugins: { allow: [runtime.id], entries: { [runtime.id]: { enabled: true } } } }, env: {}, installRecords: {}, onlyPluginIds: [runtime.id], activate: false, channelPluginLoadIntent, manifestRegistry: createPluginMetadataSnapshotFixture({ plugins: [ { id: runtime.id, origin: "global", rootDir: runtime.dir, source: runtime.file, channels: ["setup-channel"], configSchema: EMPTY_PLUGIN_SCHEMA, setupSource: path.join(runtime.dir, `${label}.cjs`), providerDiscoverySource: path.join(runtime.dir, `${label}.cjs`), capabilityCatalogSource: path.join(runtime.dir, `${label}.cjs`), }, ], }).manifestRegistry, }); const old = loadOpenClawPlugins(options("old")); const next = loadOpenClawPlugins(options("new")); expect(old.plugins[0]?.status).toBe("loaded"); setActivePluginRegistry(old, "old"); const reused = getLoadedRuntimePluginRegistry({ loadOptions: options("new") }); if (channelPluginLoadIntent === "setup") { expect([old.channels[0]?.plugin.meta.label, next.channels[0]?.plugin.meta.label]).toEqual([ "old", "new", ]); expect(reused === undefined).toBe(true); } else { expect(old.channels).toEqual([]); expect(reused === old).toBe(true); } }, ); it.each([ { sourceFormat: "cts", builtFormat: "cjs", preferBuiltPluginArtifacts: false }, { sourceFormat: "cts", builtFormat: "cjs", preferBuiltPluginArtifacts: true }, { sourceFormat: "mts", builtFormat: "mjs", preferBuiltPluginArtifacts: false }, { sourceFormat: "mts", builtFormat: "mjs", preferBuiltPluginArtifacts: true }, ])( "reuses selected bundled setup $sourceFormat/$builtFormat artifacts with built=$preferBuiltPluginArtifacts", ({ sourceFormat, builtFormat, preferBuiltPluginArtifacts }) => { const schema = EMPTY_PLUGIN_SCHEMA; const packageRoot = makePluginLoaderTempDir(); const options = [ { format: sourceFormat, tree: "extensions" }, { format: builtFormat, tree: "dist/extensions" }, { format: builtFormat, tree: "dist-runtime/extensions" }, ].map(({ format, tree }) => { const rootDir = path.join(packageRoot, tree, "bundled-setup"); mkdirSync(rootDir, { recursive: true }); const source = path.join(rootDir, `index.${format}`); const setupSource = path.join(rootDir, `setup.${format}`); const label = tree === "extensions" ? "source" : "built"; const exported = ["mts", "mjs"].includes(format) ? "export default" : "module.exports ="; writeFileSync(source, `${exported} { id: "bundled-setup", register() {} };`); writeFileSync( setupSource, `${exported} { plugin: { id: "bundled-setup", meta: { id: "bundled-setup", label: ${JSON.stringify(label)}, selectionLabel: "Fixture", docsPath: "/synthetic", blurb: "Synthetic fixture" }, capabilities: { chatTypes: ["direct"] }, config: { listAccountIds: () => [], resolveAccount: () => ({}) }, }};`, ); writeFileSync( path.join(rootDir, "package.json"), JSON.stringify({ openclaw: { extensions: [`./index.${format}`], setupEntry: `./setup.${format}` }, }), ); writeFileSync( path.join(rootDir, "openclaw.plugin.json"), JSON.stringify({ id: "bundled-setup", channels: ["bundled-setup"], configSchema: schema }), ); return { config: { plugins: { allow: ["bundled-setup"], entries: { "bundled-setup": { enabled: true } } }, }, env: {}, installRecords: {}, onlyPluginIds: ["bundled-setup"], activate: false, preferBuiltPluginArtifacts, channelPluginLoadIntent: "setup" as const, manifestRegistry: createPluginMetadataSnapshotFixture({ plugins: [ { id: "bundled-setup", origin: "bundled", rootDir, source, setupSource, channels: ["bundled-setup"], configSchema: schema, }, ], }).manifestRegistry, }; }); const expectedLabel = (index: number) => preferBuiltPluginArtifacts || index !== 0 ? "built" : "source"; for (const [loadedIndex, loadedOptions] of options.entries()) { const loaded = loadOpenClawPlugins(loadedOptions); expect(loaded.channels[0]?.plugin.meta.label).toBe(expectedLabel(loadedIndex)); setActivePluginRegistry(loaded, "canonical-view"); for (const [requestedIndex, requestedOptions] of options.entries()) { expect(getLoadedRuntimePluginRegistry({ loadOptions: requestedOptions }) === loaded).toBe( expectedLabel(loadedIndex) === expectedLabel(requestedIndex), ); } } }, ); it("does not reuse a different explicitly selected bundled entry", () => { const runtime = writePlugin({ id: "entry-fixture", filename: "index.cjs", registration: `api.registerProvider({ id: "entry-provider", label: "Old", auth: [] });`, }); const selectedSource = path.join(runtime.dir, "selected.cjs"); writeFileSync( selectedSource, `module.exports = { id: "entry-fixture", register(api) { api.registerProvider({ id: "entry-provider", label: "Selected", auth: [] }); }};`, ); const options = (source: string) => ({ config: { plugins: { allow: [runtime.id], entries: { [runtime.id]: { enabled: true } } } }, env: {}, installRecords: {}, onlyPluginIds: [runtime.id], activate: false, manifestRegistry: createPluginMetadataSnapshotFixture({ plugins: [ { id: runtime.id, origin: "bundled", rootDir: runtime.dir, source, sourcePreferred: true, providers: ["entry-provider"], configSchema: EMPTY_PLUGIN_SCHEMA, }, ], }).manifestRegistry, }); const old = loadOpenClawPlugins(options(runtime.file)); expect(old.providers[0]?.provider.label).toBe("Old"); setActivePluginRegistry(old, "old-entry"); expect( getLoadedRuntimePluginRegistry({ loadOptions: options(selectedSource) }) === undefined, ).toBe(true); expect(loadOpenClawPlugins(options(selectedSource)).providers[0]?.provider.label).toBe( "Selected", ); }); it("compares the executed artifact after the loader's final staging pass", () => { const packageRoot = makePluginLoaderTempDir(); let source = path.join( packageRoot, "dist-runtime/extensions/a/dist-runtime/extensions/b/dist-runtime/extensions/c/index.cjs", ); const selectedSource = source; for (let stage = 0; stage <= 3; stage += 1) { mkdirSync(path.dirname(source), { recursive: true }); writeFileSync( source, `module.exports = { id: "staged-fixture", register(api) { api.registerProvider({ id: "staged-provider", label: "stage-${stage}", auth: [] }); }};`, ); source = source.replace( `${path.sep}dist-runtime${path.sep}extensions${path.sep}`, `${path.sep}dist${path.sep}extensions${path.sep}`, ); } const options = { config: { plugins: { allow: ["staged-fixture"], entries: { "staged-fixture": { enabled: true } } }, }, env: {}, installRecords: {}, onlyPluginIds: ["staged-fixture"], activate: false, preferBuiltPluginArtifacts: false, manifestRegistry: createPluginMetadataSnapshotFixture({ plugins: [ { id: "staged-fixture", origin: "bundled", rootDir: path.dirname(selectedSource), source: selectedSource, providers: ["staged-provider"], configSchema: EMPTY_PLUGIN_SCHEMA, }, ], }).manifestRegistry, }; const loaded = loadOpenClawPlugins(options); expect(loaded.providers[0]?.provider.label).toBe("stage-3"); setActivePluginRegistry(loaded, "executed-stage"); expect(getLoadedRuntimePluginRegistry({ loadOptions: options }) === loaded).toBe(true); });