Files
openclaw/src/plugins/plugin-lookup-table.test.ts
Peter Steinberger 3fb201c5a7 fix(plugins): reuse gateway lifecycle metadata and isolate plugin state (#114476)
* fix(plugins): reuse gateway metadata and preserve reload isolation

* test(plugins): split lifecycle regressions and simplify gateway dispatch

* perf(plugins): reuse proven immutable snapshot graphs

* test(plugins): isolate snapshot lifecycle regression mocks

* perf(plugins): retain one gateway metadata cache

* test(plugins): preserve frozen proxy descriptor invariants

* test(plugins): isolate lifecycle metadata mock ownership

* test(plugins): bind shared worker mocks before module evaluation
2026-07-27 07:48:13 -04:00

995 lines
31 KiB
TypeScript

/** 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 type { PluginRegistrySnapshot } from "./plugin-registry.js";
const {
listPotentialConfiguredChannelIds,
listExplicitlyDisabledChannelIdsForConfig,
loadPluginManifestRegistryForInstalledIndex,
} = vi.hoisted(() => {
// Shared plugin workers must load the lookup graph under this file's manifest mocks.
vi.resetModules();
return {
listPotentialConfiguredChannelIds: vi.fn(),
listExplicitlyDisabledChannelIdsForConfig: vi.fn(),
loadPluginManifestRegistryForInstalledIndex: 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(() => {
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("memoizes prepared lookup tables by metadata snapshot and startup scope", async () => {
const plugins = [
createManifestRecord({
id: "telegram",
origin: "bundled",
channels: ["telegram"],
}),
];
const config = {
plugins: { slots: { memory: "none" } },
} as OpenClawConfig;
const env = { TELEGRAM_FAKE_TEST_TRIGGER: "configured" } as NodeJS.ProcessEnv;
const index = createIndex(plugins, {
policyHash: resolveInstalledPluginIndexPolicyHash(config),
});
loadPluginManifestRegistryForInstalledIndex.mockReturnValue({
plugins,
diagnostics: [],
});
listPotentialConfiguredChannelIds.mockImplementation(
(
_config: OpenClawConfig,
_env: NodeJS.ProcessEnv,
options?: { ambientEnvTriggers?: string },
) => (options?.ambientEnvTriggers === "suppress" ? [] : ["telegram"]),
);
const { loadPluginMetadataSnapshot } = await import("./plugin-metadata-snapshot.js");
const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js");
const metadataSnapshot = loadPluginMetadataSnapshot({ config, env, index });
const ambient = loadPluginLookUpTable({ config, env, index, metadataSnapshot });
const repeatedAmbient = loadPluginLookUpTable({ config, env, index, metadataSnapshot });
const suppressed = loadPluginLookUpTable({
config,
env,
index,
metadataSnapshot,
ambientEnvTriggers: "suppress",
});
const repeatedSuppressed = loadPluginLookUpTable({
config,
env,
index,
metadataSnapshot,
ambientEnvTriggers: "suppress",
});
expect(repeatedAmbient).toBe(ambient);
expect(repeatedSuppressed).toBe(suppressed);
expect(suppressed).not.toBe(ambient);
expect(ambient.startup.pluginIds).toEqual(["telegram"]);
expect(suppressed.startup.pluginIds).toStrictEqual([]);
expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce();
});
it("excludes ambient-only channels from the suppressed gateway startup plan", async () => {
const plugins = [
createManifestRecord({
id: "telegram",
origin: "bundled",
channels: ["telegram"],
}),
];
const index = createIndex(plugins);
const manifestRegistry: PluginManifestRegistry = { plugins, diagnostics: [] };
loadPluginManifestRegistryForInstalledIndex.mockReturnValue(manifestRegistry);
listPotentialConfiguredChannelIds.mockImplementation(
(
_config: OpenClawConfig,
_env: NodeJS.ProcessEnv,
options?: { ambientEnvTriggers?: string },
) => (options?.ambientEnvTriggers === "suppress" ? [] : ["telegram"]),
);
const { loadPluginLookUpTable } = await import("./plugin-lookup-table.js");
const config = { plugins: { slots: { memory: "none" } } } as OpenClawConfig;
const env = { TELEGRAM_FAKE_TEST_TRIGGER: "configured" } as NodeJS.ProcessEnv;
expect(loadPluginLookUpTable({ config, env, index }).startup.pluginIds).toEqual(["telegram"]);
expect(
loadPluginLookUpTable({
config,
env,
index,
ambientEnvTriggers: "suppress",
}).startup.pluginIds,
).toStrictEqual([]);
expect(
loadPluginLookUpTable({
config: {
...config,
channels: { telegram: { enabled: true } },
} as OpenClawConfig,
env,
index,
ambientEnvTriggers: "suppress",
}).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("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);
});
});