/** Tests plugin lookup table indexing for manifest-owned contribution ids. */ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js"; import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js"; import { clearLoadPluginMetadataSnapshotMemo } from "./plugin-metadata-snapshot.js"; import type { PluginRegistrySnapshot } from "./plugin-registry.js"; const listPotentialConfiguredChannelIds = vi.hoisted(() => vi.fn()); const listExplicitlyDisabledChannelIdsForConfig = vi.hoisted(() => vi.fn()); const loadPluginManifestRegistryForInstalledIndex = vi.hoisted(() => vi.fn()); vi.mock("../channels/config-presence.js", () => ({ hasMeaningfulChannelConfig: (value: unknown) => Boolean( value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).some((key) => key !== "enabled"), ), listPotentialConfiguredChannelIds: ( config: OpenClawConfig, env: NodeJS.ProcessEnv, options?: { includePersistedAuthState?: boolean }, ) => listPotentialConfiguredChannelIds(config, env, options), listExplicitlyDisabledChannelIdsForConfig: (config: OpenClawConfig) => listExplicitlyDisabledChannelIdsForConfig(config), })); vi.mock("./manifest-registry-installed.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, loadPluginManifestRegistryForInstalledIndex: (params: unknown) => loadPluginManifestRegistryForInstalledIndex(params), }; }); function createManifestRecord( plugin: Partial & Pick, ): PluginManifestRecord { return { name: plugin.id, channels: [], providers: [], cliBackends: [], skills: [], hooks: [], rootDir: `/plugins/${plugin.id}`, source: `/plugins/${plugin.id}/index.js`, manifestPath: `/plugins/${plugin.id}/openclaw.plugin.json`, ...plugin, }; } function createIndex( plugins: readonly PluginManifestRecord[], params: { policyHash?: string } = {}, ): PluginRegistrySnapshot { return { version: 1, hostContractVersion: "test", compatRegistryVersion: "test", migrationVersion: 1, policyHash: params.policyHash ?? "policy", generatedAtMs: 1, installRecords: {}, diagnostics: [], plugins: plugins.map((plugin) => ({ pluginId: plugin.id, manifestPath: plugin.manifestPath, manifestHash: `${plugin.id}-hash`, rootDir: plugin.rootDir, origin: plugin.origin, enabled: true, ...(plugin.enabledByDefault !== undefined ? { enabledByDefault: plugin.enabledByDefault } : {}), startup: { sidecar: false, memory: false, deferConfiguredChannelFullLoadUntilAfterListen: Boolean( plugin.startupDeferConfiguredChannelFullLoadUntilAfterListen, ), agentHarnesses: [], configPaths: plugin.activation?.onConfigPaths ?? [], }, compat: [], })), }; } const indexDiagnostic = { level: "warn", source: "/plugins/demo/openclaw.plugin.json", message: "indexed warning", } as const; const manifestDiagnostic = { level: "warn", source: "/plugins/demo/openclaw.plugin.json", message: "manifest warning", } as const; async function expectStaleMetadataSnapshotRebuild(params: { config: OpenClawConfig; snapshotPlugins: readonly PluginManifestRecord[]; requestedPlugins?: readonly PluginManifestRecord[]; snapshotEnv?: NodeJS.ProcessEnv; requestedEnv?: NodeJS.ProcessEnv; }) { const requestedPlugins = params.requestedPlugins ?? params.snapshotPlugins; const snapshotEnv = params.snapshotEnv ?? {}; const requestedEnv = params.requestedEnv ?? {}; const policyHash = resolveInstalledPluginIndexPolicyHash(params.config); const snapshotIndex = createIndex(params.snapshotPlugins, { policyHash }); const requestedIndex = createIndex(requestedPlugins, { policyHash }); const snapshotRegistry: PluginManifestRegistry = { plugins: [...params.snapshotPlugins], diagnostics: [], }; const requestedRegistry: PluginManifestRegistry = { plugins: [...requestedPlugins], diagnostics: [], }; loadPluginManifestRegistryForInstalledIndex .mockReturnValueOnce(snapshotRegistry) .mockReturnValue(requestedRegistry); const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); const metadataSnapshot = loadPluginMetadataSnapshot({ config: params.config, env: snapshotEnv, index: snapshotIndex, }); loadPluginManifestRegistryForInstalledIndex.mockClear(); const table = loadPluginLookUpTable({ config: params.config, env: requestedEnv, index: requestedIndex, metadataSnapshot, }); expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce(); expect(loadPluginManifestRegistryForInstalledIndex.mock.calls).toEqual([ [ { index: requestedIndex, config: params.config, workspaceDir: undefined, env: requestedEnv, includeDisabled: true, }, ], ]); return { table, requestedRegistry }; } describe("loadPluginLookUpTable", () => { beforeEach(() => { clearLoadPluginMetadataSnapshotMemo(); listPotentialConfiguredChannelIds .mockReset() .mockImplementation((config: OpenClawConfig) => Object.keys(config.channels ?? {})); listExplicitlyDisabledChannelIdsForConfig.mockReset().mockReturnValue([]); loadPluginManifestRegistryForInstalledIndex.mockReset(); }); it("builds owner maps and startup ids from one installed manifest registry", async () => { const plugins = [ createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], channelConfigs: { telegram: { schema: { type: "object" }, }, }, commandAliases: [{ name: "telegram-send" }], contracts: { tools: ["telegram.send"], }, }), createManifestRecord({ id: "openai", origin: "bundled", providers: ["openai"], providerAuthAliases: { openai: "openai", }, modelCatalog: { aliases: { "azure-openai-responses": { provider: "openai", }, }, providers: { openai: { models: [{ id: "gpt-test" }], }, }, }, cliBackends: [], setup: { providers: [{ id: "openai" }], }, }), ]; const index = { ...createIndex(plugins), diagnostics: [indexDiagnostic], }; const manifestRegistry: PluginManifestRegistry = { plugins, diagnostics: [indexDiagnostic, manifestDiagnostic], }; loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry); const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); const table = loadPluginLookUpTable({ config: { channels: { telegram: { token: "configured" }, }, plugins: { slots: { memory: "none" }, }, } as OpenClawConfig, env: {}, index, }); expect(table.manifestRegistry).toBe(manifestRegistry); expect(table.diagnostics).toEqual([indexDiagnostic, manifestDiagnostic]); expect(table.metrics.indexPluginCount).toBe(2); expect(table.metrics.manifestPluginCount).toBe(2); expect(table.metrics.startupPluginCount).toBe(1); expect(table.metrics.deferredChannelPluginCount).toBe(0); for (const metricName of [ "registrySnapshotMs", "manifestRegistryMs", "startupPlanMs", "ownerMapsMs", "totalMs", ] as const) { expect(table.metrics[metricName]).toBeGreaterThanOrEqual(0); } expect(table.byPluginId.get("telegram")?.id).toBe("telegram"); expect(table.normalizePluginId("openai")).toBe("openai"); expect(table.owners.channels.get("telegram")).toEqual(["telegram"]); expect(table.owners.channelConfigs.get("telegram")).toEqual(["telegram"]); expect(table.owners.providers.get("openai")).toEqual(["openai"]); expect(table.owners.providers.get("openai")).toEqual(["openai"]); expect(table.owners.modelCatalogProviders.get("openai")).toEqual(["openai"]); expect(table.owners.modelCatalogProviders.get("azure-openai-responses")).toEqual(["openai"]); expect(table.owners.cliBackends.get("codex-cli")).toBeUndefined(); expect(table.owners.setupProviders.get("openai")).toEqual(["openai"]); expect(table.owners.commandAliases.get("telegram-send")).toEqual(["telegram"]); expect(table.owners.contracts.get("tools")).toEqual(["telegram"]); expect(table.startup.channelPluginIds).toEqual(["telegram"]); expect(table.startup.configuredDeferredChannelPluginIds).toStrictEqual([]); expect(table.startup.pluginIds).toEqual(["telegram"]); }); it("scopes metadata manifest reconstruction for restrictive startup allowlists", async () => { const plugins = [ createManifestRecord({ id: "openai", origin: "bundled", enabledByDefault: true, providers: ["openai"], activation: { onStartup: true, }, }), createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), createManifestRecord({ id: "discord", origin: "bundled", channels: ["discord"], }), ]; const index = createIndex(plugins); loadPluginManifestRegistryForInstalledIndex.mockImplementation( (params: { pluginIds?: readonly string[] }) => ({ plugins: params.pluginIds ? plugins.filter((plugin) => params.pluginIds?.includes(plugin.id)) : plugins, diagnostics: [], }), ); const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); const table = loadPluginLookUpTable({ config: { plugins: { allow: ["openai"], slots: { memory: "none" }, }, } as OpenClawConfig, env: {}, index, }); expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce(); expect(loadPluginManifestRegistryForInstalledIndex.mock.calls[0]?.[0]).toMatchObject({ index, config: { plugins: { allow: ["openai"], slots: { memory: "none" }, }, }, env: {}, includeDisabled: true, pluginIds: ["openai"], }); expect(table.pluginIds).toEqual(["openai"]); expect(table.metrics.indexPluginCount).toBe(3); expect(table.metrics.manifestPluginCount).toBe(1); expect(table.byPluginId.has("telegram")).toBe(false); expect(table.startup.pluginIds).toEqual(["openai"]); }); it("keeps config-path startup activation owners in scoped manifest reconstruction", async () => { const plugins = [ createManifestRecord({ id: "openai", origin: "bundled", enabledByDefault: true, providers: ["openai"], activation: { onStartup: true, }, }), createManifestRecord({ id: "browser", origin: "bundled", enabledByDefault: true, activation: { onStartup: true, onConfigPaths: ["browser"], }, }), createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), ]; const index = createIndex(plugins); loadPluginManifestRegistryForInstalledIndex.mockImplementation( (params: { pluginIds?: readonly string[] }) => ({ plugins: params.pluginIds ? plugins.filter((plugin) => params.pluginIds?.includes(plugin.id)) : plugins, diagnostics: [], }), ); const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); const table = loadPluginLookUpTable({ config: { browser: { enabled: true, }, plugins: { allow: ["openai"], slots: { memory: "none" }, }, } as OpenClawConfig, env: {}, index, }); expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce(); expect(loadPluginManifestRegistryForInstalledIndex.mock.calls[0]?.[0]).toMatchObject({ pluginIds: ["browser", "openai"], }); expect(table.pluginIds).toEqual(["browser", "openai"]); expect(table.metrics.indexPluginCount).toBe(3); expect(table.metrics.manifestPluginCount).toBe(2); expect(table.byPluginId.has("telegram")).toBe(false); expect(table.startup.pluginIds).toEqual(["openai", "browser"]); }); it("rebuilds an unscoped provided snapshot for restrictive startup scopes", async () => { const plugins = [ createManifestRecord({ id: "openai", origin: "bundled", enabledByDefault: true, providers: ["openai"], activation: { onStartup: true, }, }), createManifestRecord({ id: "browser", origin: "bundled", enabledByDefault: true, activation: { onStartup: true, onConfigPaths: ["browser"], }, }), createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), ]; const config = { browser: { enabled: true, }, plugins: { allow: ["openai"], slots: { memory: "none" }, }, } as OpenClawConfig; const index = createIndex(plugins, { policyHash: resolveInstalledPluginIndexPolicyHash(config), }); loadPluginManifestRegistryForInstalledIndex.mockImplementation( (params: { pluginIds?: readonly string[] }) => ({ plugins: params.pluginIds ? plugins.filter((plugin) => params.pluginIds?.includes(plugin.id)) : plugins, diagnostics: [], }), ); const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); const metadataSnapshot = loadPluginMetadataSnapshot({ config, env: {}, index, }); expect(metadataSnapshot.pluginIds).toBeUndefined(); expect(metadataSnapshot.metrics.manifestPluginCount).toBe(3); loadPluginManifestRegistryForInstalledIndex.mockClear(); const table = loadPluginLookUpTable({ config, env: {}, index, metadataSnapshot, }); expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce(); expect(loadPluginManifestRegistryForInstalledIndex.mock.calls[0]?.[0]).toMatchObject({ pluginIds: ["browser", "openai"], }); expect(table.pluginIds).toEqual(["browser", "openai"]); expect(table.metrics.indexPluginCount).toBe(3); expect(table.metrics.manifestPluginCount).toBe(2); expect(table.byPluginId.has("telegram")).toBe(false); expect(table.startup.pluginIds).toEqual(["openai", "browser"]); }); it("reuses a scoped provided metadata snapshot when it covers the startup scope", async () => { const plugins = [ createManifestRecord({ id: "openai", origin: "bundled", enabledByDefault: true, providers: ["openai"], activation: { onStartup: true, }, }), createManifestRecord({ id: "browser", origin: "bundled", enabledByDefault: true, activation: { onStartup: true, onConfigPaths: ["browser"], }, }), createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), ]; const config = { browser: { enabled: false, }, plugins: { allow: ["openai"], entries: { browser: { enabled: false }, openai: { enabled: true }, }, slots: { memory: "none" }, }, } as OpenClawConfig; const index = createIndex(plugins, { policyHash: resolveInstalledPluginIndexPolicyHash(config), }); loadPluginManifestRegistryForInstalledIndex.mockImplementation( (params: { pluginIds?: readonly string[] }) => ({ plugins: params.pluginIds ? plugins.filter((plugin) => params.pluginIds?.includes(plugin.id)) : plugins, diagnostics: [], }), ); const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); const metadataSnapshot = loadPluginMetadataSnapshot({ config, env: {}, index, pluginIds: ["browser", "openai"], }); expect(metadataSnapshot.pluginIds).toEqual(["browser", "openai"]); expect(metadataSnapshot.metrics.manifestPluginCount).toBe(2); loadPluginManifestRegistryForInstalledIndex.mockClear(); const table = loadPluginLookUpTable({ config, env: {}, index, metadataSnapshot, }); expect(loadPluginManifestRegistryForInstalledIndex).not.toHaveBeenCalled(); expect(table.pluginIds).toEqual(["browser", "openai"]); expect(table.metrics.indexPluginCount).toBe(3); expect(table.metrics.manifestPluginCount).toBe(2); expect(table.byPluginId.has("telegram")).toBe(false); expect(table.startup.pluginIds).toEqual(["openai"]); }); it("rebuilds a non-empty scoped provided snapshot for an empty startup scope", async () => { const plugins = [ createManifestRecord({ id: "openai", origin: "bundled", enabledByDefault: true, providers: ["openai"], activation: { onStartup: true, }, }), ]; const config = { plugins: { enabled: false, }, } as OpenClawConfig; const index = createIndex(plugins, { policyHash: resolveInstalledPluginIndexPolicyHash(config), }); loadPluginManifestRegistryForInstalledIndex.mockImplementation( (params: { pluginIds?: readonly string[] }) => ({ plugins: params.pluginIds ? plugins.filter((plugin) => params.pluginIds?.includes(plugin.id)) : plugins, diagnostics: [], }), ); const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); const metadataSnapshot = loadPluginMetadataSnapshot({ config, env: {}, index, pluginIds: ["openai"], }); loadPluginManifestRegistryForInstalledIndex.mockClear(); const table = loadPluginLookUpTable({ config, env: {}, index, metadataSnapshot, }); expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce(); expect(loadPluginManifestRegistryForInstalledIndex.mock.calls[0]?.[0]).toMatchObject({ pluginIds: [], }); expect(table.pluginIds).toEqual([]); expect(table.metrics.manifestPluginCount).toBe(0); expect(table.startup.pluginIds).toEqual([]); }); it("derives startup ids from a provided metadata snapshot without reloading manifests", async () => { const plugins = [ createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), ]; const index = createIndex(plugins); const config = { channels: { telegram: { token: "configured" }, }, } as OpenClawConfig; const compatibleIndex = { ...index, policyHash: resolveInstalledPluginIndexPolicyHash(config), }; const manifestRegistry: PluginManifestRegistry = { plugins, diagnostics: [], }; loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry); const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); const metadataSnapshot = loadPluginMetadataSnapshot({ config, env: {}, index: compatibleIndex, }); loadPluginManifestRegistryForInstalledIndex.mockClear(); const table = loadPluginLookUpTable({ config, env: {}, metadataSnapshot, }); expect(loadPluginManifestRegistryForInstalledIndex).not.toHaveBeenCalled(); expect(table.manifestRegistry).toBe(manifestRegistry); expect(table.startup.pluginIds).toEqual(["telegram"]); expect(table.metrics.indexPluginCount).toBe(1); expect(table.metrics.manifestPluginCount).toBe(1); expect(table.metrics.totalMs).toBe( metadataSnapshot.metrics.totalMs + table.metrics.startupPlanMs, ); }); it("reuses lookup tables prepared from the same metadata snapshot and activation config", async () => { const plugins = [ createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), ]; const config = { channels: { telegram: { token: "configured" }, }, } as OpenClawConfig; const index = { ...createIndex(plugins), policyHash: resolveInstalledPluginIndexPolicyHash(config), }; const manifestRegistry: PluginManifestRegistry = { plugins, diagnostics: [], }; loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry); const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); const { clearPluginLookUpTableMemoForTest, loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); clearPluginLookUpTableMemoForTest(); const metadataSnapshot = loadPluginMetadataSnapshot({ config, env: {}, index, }); const manifestRegistryLoadCount = loadPluginManifestRegistryForInstalledIndex.mock.calls.length; listPotentialConfiguredChannelIds.mockClear(); const first = loadPluginLookUpTable({ config, env: {}, metadataSnapshot, }); const second = loadPluginLookUpTable({ config, env: {}, metadataSnapshot, }); expect(second).toBe(first); expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledTimes( manifestRegistryLoadCount, ); }); it("reuses lookup tables when unrelated env values change", async () => { const plugins = [ createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), ]; const config = {} as OpenClawConfig; const index = { ...createIndex(plugins), policyHash: resolveInstalledPluginIndexPolicyHash(config), }; const manifestRegistry: PluginManifestRegistry = { plugins, diagnostics: [], }; loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry); const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); const { clearPluginLookUpTableMemoForTest, loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); clearPluginLookUpTableMemoForTest(); const metadataSnapshot = loadPluginMetadataSnapshot({ config, env: { UNRELATED_ONE: "1" }, index, }); const first = loadPluginLookUpTable({ config, env: { UNRELATED_ONE: "1" }, metadataSnapshot, }); const second = loadPluginLookUpTable({ config, env: { UNRELATED_TWO: "2" }, metadataSnapshot, }); expect(second).toBe(first); }); it("does not reuse lookup tables when channel env presence changes startup scope", async () => { listPotentialConfiguredChannelIds.mockImplementation( (_config: OpenClawConfig, env: NodeJS.ProcessEnv) => (env.TELEGRAM_TOKEN ? ["telegram"] : []), ); const plugins = [ createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), ]; const config = {} as OpenClawConfig; const index = { ...createIndex(plugins), policyHash: resolveInstalledPluginIndexPolicyHash(config), }; const manifestRegistry: PluginManifestRegistry = { plugins, diagnostics: [], }; loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry); const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); const { clearPluginLookUpTableMemoForTest, loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); clearPluginLookUpTableMemoForTest(); const metadataSnapshot = loadPluginMetadataSnapshot({ config, env: {}, index, }); const first = loadPluginLookUpTable({ config, env: {}, metadataSnapshot, }); const second = loadPluginLookUpTable({ config, env: { TELEGRAM_TOKEN: "configured" }, metadataSnapshot, }); expect(second).not.toBe(first); expect(first.startup.pluginIds).toStrictEqual([]); expect(second.startup.pluginIds).toStrictEqual(["telegram"]); }); it("rebuilds when a provided metadata snapshot has a stale plugin policy", async () => { const plugins = [ createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), ]; const snapshotConfig = { plugins: { allow: ["telegram"], }, } as OpenClawConfig; const requestedConfig = { plugins: { allow: ["other-plugin"], }, } as OpenClawConfig; const snapshotIndex = createIndex(plugins, { policyHash: resolveInstalledPluginIndexPolicyHash(snapshotConfig), }); const requestedIndex = createIndex(plugins, { policyHash: resolveInstalledPluginIndexPolicyHash(requestedConfig), }); const manifestRegistry: PluginManifestRegistry = { plugins, diagnostics: [], }; loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry); const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); const metadataSnapshot = loadPluginMetadataSnapshot({ config: snapshotConfig, env: {}, index: snapshotIndex, }); loadPluginManifestRegistryForInstalledIndex.mockClear(); loadPluginLookUpTable({ config: requestedConfig, env: {}, index: requestedIndex, metadataSnapshot, }); expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce(); expect(loadPluginManifestRegistryForInstalledIndex.mock.calls).toEqual([ [ { index: requestedIndex, config: requestedConfig, workspaceDir: undefined, env: {}, includeDisabled: true, }, ], ]); }); it("rebuilds when a provided metadata snapshot has stale plugin load paths", async () => { const plugins = [ createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), ]; const snapshotConfig = { plugins: { load: { paths: ["/plugins/one"] }, }, } as OpenClawConfig; const requestedConfig = { plugins: { load: { paths: ["/plugins/two"] }, }, } as OpenClawConfig; const policyHash = resolveInstalledPluginIndexPolicyHash(snapshotConfig); const index = createIndex(plugins, { policyHash }); const manifestRegistry: PluginManifestRegistry = { plugins, diagnostics: [], }; loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry); const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js"); const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js"); const metadataSnapshot = loadPluginMetadataSnapshot({ config: snapshotConfig, env: {}, index, }); loadPluginManifestRegistryForInstalledIndex.mockClear(); loadPluginLookUpTable({ config: requestedConfig, env: {}, index, metadataSnapshot, }); expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce(); expect(loadPluginManifestRegistryForInstalledIndex.mock.calls).toEqual([ [ { index, config: requestedConfig, workspaceDir: undefined, env: {}, includeDisabled: true, }, ], ]); }); it("rebuilds when a provided metadata snapshot has stale env-resolved plugin load paths", async () => { const plugins = [ createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), ]; const config = { plugins: { load: { paths: ["~/plugins"] }, }, } as OpenClawConfig; const snapshotEnv = { HOME: "/home/snapshot", OPENCLAW_HOME: undefined, } as NodeJS.ProcessEnv; const requestedEnv = { HOME: "/home/requested", OPENCLAW_HOME: undefined, } as NodeJS.ProcessEnv; await expectStaleMetadataSnapshotRebuild({ config, snapshotPlugins: plugins, snapshotEnv, requestedEnv, }); }); it("rebuilds when a provided metadata snapshot has stale env-resolved plugin roots", async () => { const plugins = [ createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), ]; const config = {} as OpenClawConfig; const snapshotEnv = { HOME: "/home/snapshot", OPENCLAW_HOME: undefined, } as NodeJS.ProcessEnv; const requestedEnv = { HOME: "/home/requested", OPENCLAW_HOME: undefined, } as NodeJS.ProcessEnv; await expectStaleMetadataSnapshotRebuild({ config, snapshotPlugins: plugins, snapshotEnv, requestedEnv, }); }); it("rebuilds when a provided metadata snapshot has stale plugin inventory", async () => { const snapshotPlugins = [ createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), ]; const requestedPlugins = [ createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), createManifestRecord({ id: "discord", origin: "bundled", channels: ["discord"], }), ]; const config = { channels: { telegram: { token: "configured" }, }, } as OpenClawConfig; const { table, requestedRegistry } = await expectStaleMetadataSnapshotRebuild({ config, snapshotPlugins, requestedPlugins, }); expect(table.manifestRegistry).toBe(requestedRegistry); }); it("rebuilds when a provided metadata snapshot has stale plugin paths", async () => { const snapshotPlugins = [ createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], }), ]; const requestedPlugins = [ createManifestRecord({ id: "telegram", origin: "bundled", channels: ["telegram"], rootDir: "/plugins-moved/telegram", source: "/plugins-moved/telegram/index.js", manifestPath: "/plugins-moved/telegram/openclaw.plugin.json", }), ]; const config = { channels: { telegram: { token: "configured" }, }, } as OpenClawConfig; const { table, requestedRegistry } = await expectStaleMetadataSnapshotRebuild({ config, snapshotPlugins, requestedPlugins, }); expect(table.manifestRegistry).toBe(requestedRegistry); }); });