Files
openclaw/src/plugins/registry.dual-kind-memory-gate.test.ts
fuller-stack-dev 235908c30e fix: support multi-kind plugins for dual slot ownership (#57507) (thanks @fuller-stack-dev)
* feat(plugins): support multi-kind plugins for dual slot ownership

* fix: address review feedback on multi-kind plugin support

- Use sorted normalizeKinds() for kind-mismatch comparison in loader.ts
  (fixes order-sensitive JSON.stringify for arrays)
- Derive slot-to-kind reverse mapping from SLOT_BY_KIND in slots.ts
  (removes hardcoded ternary that would break for future slot types)
- Use shared hasKind() helper in config-state.ts instead of inline logic

* fix: don't disable dual-kind plugin that still owns another slot

When a new plugin takes over one slot, a dual-kind plugin that still
owns the other slot must not be disabled — otherwise context engine
resolution fails at runtime.

* fix: exempt dual-kind plugins from memory slot disablement

A plugin with kind: ["memory", "context-engine"] must stay enabled even
when it loses the memory slot, so its context engine role can still load.

* fix: address remaining review feedback

- Pass manifest kind (not hardcoded "memory") in early memory gating
- Extract kindsEqual() helper for DRY kind comparison in loader.ts
- Narrow slotKeyForPluginKind back to single PluginKind with JSDoc
- Reject empty array in parsePluginKind
- Add kindsEqual tests

* fix: use toSorted() instead of sort() per lint rules

* plugins: include default slot ownership in disable checks and gate dual-kind memory registration
2026-03-31 10:06:48 +05:30

96 lines
2.7 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import {
createPluginRegistryFixture,
registerTestPlugin,
registerVirtualTestPlugin,
} from "./contracts/testkit.js";
import { clearMemoryEmbeddingProviders } from "./memory-embedding-providers.js";
import { _resetMemoryPluginState, getMemoryRuntime } from "./memory-state.js";
import { createPluginRecord } from "./status.test-helpers.js";
afterEach(() => {
_resetMemoryPluginState();
clearMemoryEmbeddingProviders();
});
function createStubMemoryRuntime() {
return {
async getMemorySearchManager() {
return { manager: null, error: "missing" } as const;
},
resolveMemoryBackendConfig() {
return { backend: "builtin" as const };
},
};
}
describe("dual-kind memory registration gate", () => {
it("blocks memory runtime registration for dual-kind plugins not selected for memory slot", () => {
const { config, registry } = createPluginRegistryFixture();
registerVirtualTestPlugin({
registry,
config,
id: "dual-plugin",
name: "Dual Plugin",
kind: ["memory", "context-engine"],
register(api) {
api.registerMemoryRuntime(createStubMemoryRuntime());
},
});
expect(getMemoryRuntime()).toBeUndefined();
expect(registry.registry.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({
pluginId: "dual-plugin",
level: "warn",
message: expect.stringContaining("dual-kind plugin not selected for memory slot"),
}),
]),
);
});
it("allows memory runtime registration for dual-kind plugins selected for memory slot", () => {
const { config, registry } = createPluginRegistryFixture();
registerTestPlugin({
registry,
config,
record: createPluginRecord({
id: "dual-plugin",
name: "Dual Plugin",
kind: ["memory", "context-engine"],
memorySlotSelected: true,
}),
register(api) {
api.registerMemoryRuntime(createStubMemoryRuntime());
},
});
expect(getMemoryRuntime()).toBeDefined();
expect(
registry.registry.diagnostics.filter(
(d) => d.pluginId === "dual-plugin" && d.level === "warn",
),
).toHaveLength(0);
});
it("allows memory runtime registration for single-kind memory plugins without memorySlotSelected", () => {
const { config, registry } = createPluginRegistryFixture();
registerVirtualTestPlugin({
registry,
config,
id: "memory-only",
name: "Memory Only",
kind: "memory",
register(api) {
api.registerMemoryRuntime(createStubMemoryRuntime());
},
});
expect(getMemoryRuntime()).toBeDefined();
});
});