mirror of https://github.com/openclaw/openclaw.git
344 lines
10 KiB
TypeScript
344 lines
10 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import {
|
|
applyExclusiveSlotSelection,
|
|
hasKind,
|
|
kindsEqual,
|
|
normalizeKinds,
|
|
slotKeysForPluginKind,
|
|
} from "./slots.js";
|
|
import type { PluginKind } from "./types.js";
|
|
|
|
describe("applyExclusiveSlotSelection", () => {
|
|
const createMemoryConfig = (plugins?: OpenClawConfig["plugins"]): OpenClawConfig => ({
|
|
plugins: {
|
|
...plugins,
|
|
entries: {
|
|
...plugins?.entries,
|
|
memory: {
|
|
enabled: true,
|
|
...plugins?.entries?.memory,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const runMemorySelection = (config: OpenClawConfig, selectedId = "memory") =>
|
|
applyExclusiveSlotSelection({
|
|
config,
|
|
selectedId,
|
|
selectedKind: "memory",
|
|
registry: {
|
|
plugins: [
|
|
{ id: "memory-core", kind: "memory" },
|
|
{ id: "memory", kind: "memory" },
|
|
],
|
|
},
|
|
});
|
|
|
|
function expectMemorySelectionState(
|
|
result: ReturnType<typeof applyExclusiveSlotSelection>,
|
|
params: {
|
|
changed: boolean;
|
|
selectedId?: string;
|
|
disabledCompetingPlugin?: boolean;
|
|
},
|
|
) {
|
|
expect(result.changed).toBe(params.changed);
|
|
if (params.selectedId) {
|
|
expect(result.config.plugins?.slots?.memory).toBe(params.selectedId);
|
|
}
|
|
if (params.disabledCompetingPlugin != null) {
|
|
expect(result.config.plugins?.entries?.["memory-core"]?.enabled).toBe(
|
|
params.disabledCompetingPlugin,
|
|
);
|
|
}
|
|
}
|
|
|
|
function expectSelectionWarnings(
|
|
warnings: string[],
|
|
params: {
|
|
contains?: readonly string[];
|
|
excludes?: readonly string[];
|
|
},
|
|
) {
|
|
if (params.contains?.length) {
|
|
expect(warnings).toEqual(expect.arrayContaining([...params.contains]));
|
|
}
|
|
for (const warning of params.excludes ?? []) {
|
|
expect(warnings).not.toEqual(expect.arrayContaining([warning]));
|
|
}
|
|
}
|
|
|
|
function expectUnchangedSelection(result: ReturnType<typeof applyExclusiveSlotSelection>) {
|
|
expect(result.changed).toBe(false);
|
|
expect(result.warnings).toHaveLength(0);
|
|
}
|
|
|
|
function buildSelectionRegistry(
|
|
plugins: ReadonlyArray<{ id: string; kind?: PluginKind | PluginKind[] }>,
|
|
) {
|
|
return {
|
|
plugins: [...plugins],
|
|
};
|
|
}
|
|
|
|
function expectUnchangedSelectionCase(params: {
|
|
config: OpenClawConfig;
|
|
selectedId: string;
|
|
selectedKind?: PluginKind | PluginKind[];
|
|
registry?: { plugins: ReadonlyArray<{ id: string; kind?: PluginKind | PluginKind[] }> };
|
|
}) {
|
|
const result = applyExclusiveSlotSelection({
|
|
config: params.config,
|
|
selectedId: params.selectedId,
|
|
...(params.selectedKind ? { selectedKind: params.selectedKind } : {}),
|
|
...(params.registry
|
|
? {
|
|
registry: buildSelectionRegistry(params.registry.plugins),
|
|
}
|
|
: {}),
|
|
});
|
|
|
|
expectUnchangedSelection(result);
|
|
expect(result.config).toBe(params.config);
|
|
}
|
|
|
|
function expectChangedSelectionCase(params: {
|
|
config: OpenClawConfig;
|
|
selectedId?: string;
|
|
expectedDisabled?: boolean;
|
|
warningChecks: {
|
|
contains?: readonly string[];
|
|
excludes?: readonly string[];
|
|
};
|
|
}) {
|
|
const result = runMemorySelection(params.config, params.selectedId);
|
|
|
|
expectMemorySelectionState(result, {
|
|
changed: true,
|
|
selectedId: params.selectedId ?? "memory",
|
|
...(params.expectedDisabled != null
|
|
? { disabledCompetingPlugin: params.expectedDisabled }
|
|
: {}),
|
|
});
|
|
expectSelectionWarnings(result.warnings, params.warningChecks);
|
|
}
|
|
|
|
it.each([
|
|
{
|
|
name: "selects the slot and disables other entries for the same kind",
|
|
config: createMemoryConfig({
|
|
slots: { memory: "memory-core" },
|
|
entries: { "memory-core": { enabled: true } },
|
|
}),
|
|
expectedDisabled: false,
|
|
warningChecks: {
|
|
contains: [
|
|
'Exclusive slot "memory" switched from "memory-core" to "memory".',
|
|
'Disabled other "memory" slot plugins: memory-core.',
|
|
],
|
|
},
|
|
},
|
|
{
|
|
name: "warns when the slot falls back to a default",
|
|
config: createMemoryConfig(),
|
|
warningChecks: {
|
|
contains: ['Exclusive slot "memory" switched from "memory-core" to "memory".'],
|
|
},
|
|
},
|
|
{
|
|
name: "keeps disabled competing plugins disabled without adding disable warnings",
|
|
config: createMemoryConfig({
|
|
entries: {
|
|
"memory-core": { enabled: false },
|
|
},
|
|
}),
|
|
expectedDisabled: false,
|
|
warningChecks: {
|
|
contains: ['Exclusive slot "memory" switched from "memory-core" to "memory".'],
|
|
excludes: ['Disabled other "memory" slot plugins: memory-core.'],
|
|
},
|
|
},
|
|
] as const)("$name", ({ config, expectedDisabled, warningChecks }) => {
|
|
expectChangedSelectionCase({
|
|
config,
|
|
...(expectedDisabled != null ? { expectedDisabled } : {}),
|
|
warningChecks,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "does nothing when the slot already matches",
|
|
config: createMemoryConfig({
|
|
slots: { memory: "memory" },
|
|
}),
|
|
selectedId: "memory",
|
|
selectedKind: "memory",
|
|
registry: { plugins: [{ id: "memory", kind: "memory" }] },
|
|
},
|
|
{
|
|
name: "skips changes when no exclusive slot applies",
|
|
config: {} as OpenClawConfig,
|
|
selectedId: "custom",
|
|
},
|
|
] as const)("$name", ({ config, selectedId, selectedKind, registry }) => {
|
|
expectUnchangedSelectionCase({
|
|
config,
|
|
selectedId,
|
|
...(selectedKind ? { selectedKind } : {}),
|
|
...(registry ? { registry: buildSelectionRegistry(registry.plugins) } : {}),
|
|
});
|
|
});
|
|
|
|
it("applies slot selection for each kind in a multi-kind array", () => {
|
|
const config: OpenClawConfig = {
|
|
plugins: {
|
|
slots: { memory: "memory-core", contextEngine: "legacy" },
|
|
entries: {
|
|
"memory-core": { enabled: true },
|
|
legacy: { enabled: true },
|
|
},
|
|
},
|
|
};
|
|
const result = applyExclusiveSlotSelection({
|
|
config,
|
|
selectedId: "dual-plugin",
|
|
selectedKind: ["memory", "context-engine"],
|
|
registry: buildSelectionRegistry([
|
|
{ id: "memory-core", kind: "memory" },
|
|
{ id: "legacy", kind: "context-engine" },
|
|
{ id: "dual-plugin", kind: ["memory", "context-engine"] },
|
|
]),
|
|
});
|
|
expect(result.changed).toBe(true);
|
|
expect(result.config.plugins?.slots?.memory).toBe("dual-plugin");
|
|
expect(result.config.plugins?.slots?.contextEngine).toBe("dual-plugin");
|
|
expect(result.config.plugins?.entries?.["memory-core"]?.enabled).toBe(false);
|
|
expect(result.config.plugins?.entries?.legacy?.enabled).toBe(false);
|
|
});
|
|
|
|
it("does not disable a dual-kind plugin that still owns another slot", () => {
|
|
const config: OpenClawConfig = {
|
|
plugins: {
|
|
slots: { memory: "dual-plugin", contextEngine: "dual-plugin" },
|
|
entries: {
|
|
"dual-plugin": { enabled: true },
|
|
},
|
|
},
|
|
};
|
|
const result = applyExclusiveSlotSelection({
|
|
config,
|
|
selectedId: "new-memory",
|
|
selectedKind: "memory",
|
|
registry: buildSelectionRegistry([
|
|
{ id: "dual-plugin", kind: ["memory", "context-engine"] },
|
|
{ id: "new-memory", kind: "memory" },
|
|
]),
|
|
});
|
|
expect(result.changed).toBe(true);
|
|
expect(result.config.plugins?.slots?.memory).toBe("new-memory");
|
|
// dual-plugin still owns contextEngine — must NOT be disabled
|
|
expect(result.config.plugins?.entries?.["dual-plugin"]?.enabled).not.toBe(false);
|
|
});
|
|
|
|
it("does not disable a dual-kind plugin that owns another slot via default", () => {
|
|
// contextEngine is NOT explicitly set — defaults to "legacy"
|
|
const config: OpenClawConfig = {
|
|
plugins: {
|
|
slots: { memory: "legacy" },
|
|
entries: {
|
|
legacy: { enabled: true },
|
|
},
|
|
},
|
|
};
|
|
const result = applyExclusiveSlotSelection({
|
|
config,
|
|
selectedId: "new-memory",
|
|
selectedKind: "memory",
|
|
registry: buildSelectionRegistry([
|
|
{ id: "legacy", kind: ["memory", "context-engine"] },
|
|
{ id: "new-memory", kind: "memory" },
|
|
]),
|
|
});
|
|
expect(result.changed).toBe(true);
|
|
expect(result.config.plugins?.slots?.memory).toBe("new-memory");
|
|
// legacy still owns contextEngine via default — must NOT be disabled
|
|
expect(result.config.plugins?.entries?.legacy?.enabled).not.toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("normalizeKinds", () => {
|
|
it("returns empty array for undefined", () => {
|
|
expect(normalizeKinds(undefined)).toEqual([]);
|
|
});
|
|
|
|
it("wraps a single kind in an array", () => {
|
|
expect(normalizeKinds("memory")).toEqual(["memory"]);
|
|
});
|
|
|
|
it("returns an array kind as-is", () => {
|
|
expect(normalizeKinds(["memory", "context-engine"])).toEqual(["memory", "context-engine"]);
|
|
});
|
|
});
|
|
|
|
describe("hasKind", () => {
|
|
it("returns false for undefined kind", () => {
|
|
expect(hasKind(undefined, "memory")).toBe(false);
|
|
});
|
|
|
|
it("matches a single kind string", () => {
|
|
expect(hasKind("memory", "memory")).toBe(true);
|
|
expect(hasKind("memory", "context-engine")).toBe(false);
|
|
});
|
|
|
|
it("matches within a kind array", () => {
|
|
expect(hasKind(["memory", "context-engine"], "memory")).toBe(true);
|
|
expect(hasKind(["memory", "context-engine"], "context-engine")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("slotKeysForPluginKind", () => {
|
|
it("returns empty for undefined", () => {
|
|
expect(slotKeysForPluginKind(undefined)).toEqual([]);
|
|
});
|
|
|
|
it("returns single slot key for single kind", () => {
|
|
expect(slotKeysForPluginKind("memory")).toEqual(["memory"]);
|
|
});
|
|
|
|
it("returns multiple slot keys for multi-kind", () => {
|
|
expect(slotKeysForPluginKind(["memory", "context-engine"])).toEqual([
|
|
"memory",
|
|
"contextEngine",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("kindsEqual", () => {
|
|
it("treats undefined as equal to undefined", () => {
|
|
expect(kindsEqual(undefined, undefined)).toBe(true);
|
|
});
|
|
|
|
it("matches identical strings", () => {
|
|
expect(kindsEqual("memory", "memory")).toBe(true);
|
|
});
|
|
|
|
it("rejects different strings", () => {
|
|
expect(kindsEqual("memory", "context-engine")).toBe(false);
|
|
});
|
|
|
|
it("matches arrays in different order", () => {
|
|
expect(kindsEqual(["memory", "context-engine"], ["context-engine", "memory"])).toBe(true);
|
|
});
|
|
|
|
it("matches string against single-element array", () => {
|
|
expect(kindsEqual("memory", ["memory"])).toBe(true);
|
|
});
|
|
|
|
it("rejects mismatched lengths", () => {
|
|
expect(kindsEqual("memory", ["memory", "context-engine"])).toBe(false);
|
|
});
|
|
});
|