Files
openclaw/src/plugins/plugin-lookup-table.test.ts
WhatsSkiLL 22b8e1cf4f fix(plugins): scope startup metadata manifest reads
Limit plugin metadata snapshots to the channel, provider, and startup surfaces that need them, while preserving unscoped fallback for incomplete index data and provider runtime resolution.

Refs #70533.
Refs #84628.

Co-authored-by: IWhatsskill <IWhatsskill@users.noreply.github.com>
2026-05-31 11:58:56 +01:00

939 lines
28 KiB
TypeScript

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<typeof import("./manifest-registry-installed.js")>();
return {
...actual,
loadPluginManifestRegistryForInstalledIndex: (params: unknown) =>
loadPluginManifestRegistryForInstalledIndex(params),
};
});
function createManifestRecord(
plugin: Partial<PluginManifestRecord> & Pick<PluginManifestRecord, "id" | "origin">,
): 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("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);
});
});