mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-31 11:51:22 +00:00
test: dedupe plugin core utility suites
This commit is contained in:
@@ -6,50 +6,16 @@ import {
|
||||
} from "./config-state.js";
|
||||
|
||||
describe("normalizePluginsConfig", () => {
|
||||
it("uses default memory slot when not specified", () => {
|
||||
const result = normalizePluginsConfig({});
|
||||
expect(result.slots.memory).toBe("memory-core");
|
||||
});
|
||||
|
||||
it("respects explicit memory slot value", () => {
|
||||
const result = normalizePluginsConfig({
|
||||
slots: { memory: "custom-memory" },
|
||||
});
|
||||
expect(result.slots.memory).toBe("custom-memory");
|
||||
});
|
||||
|
||||
it("disables memory slot when set to 'none' (case insensitive)", () => {
|
||||
expect(
|
||||
normalizePluginsConfig({
|
||||
slots: { memory: "none" },
|
||||
}).slots.memory,
|
||||
).toBeNull();
|
||||
expect(
|
||||
normalizePluginsConfig({
|
||||
slots: { memory: "None" },
|
||||
}).slots.memory,
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("trims whitespace from memory slot value", () => {
|
||||
const result = normalizePluginsConfig({
|
||||
slots: { memory: " custom-memory " },
|
||||
});
|
||||
expect(result.slots.memory).toBe("custom-memory");
|
||||
});
|
||||
|
||||
it("uses default when memory slot is empty string", () => {
|
||||
const result = normalizePluginsConfig({
|
||||
slots: { memory: "" },
|
||||
});
|
||||
expect(result.slots.memory).toBe("memory-core");
|
||||
});
|
||||
|
||||
it("uses default when memory slot is whitespace only", () => {
|
||||
const result = normalizePluginsConfig({
|
||||
slots: { memory: " " },
|
||||
});
|
||||
expect(result.slots.memory).toBe("memory-core");
|
||||
it.each([
|
||||
[{}, "memory-core"],
|
||||
[{ slots: { memory: "custom-memory" } }, "custom-memory"],
|
||||
[{ slots: { memory: "none" } }, null],
|
||||
[{ slots: { memory: "None" } }, null],
|
||||
[{ slots: { memory: " custom-memory " } }, "custom-memory"],
|
||||
[{ slots: { memory: "" } }, "memory-core"],
|
||||
[{ slots: { memory: " " } }, "memory-core"],
|
||||
] as const)("normalizes memory slot for %o", (config, expected) => {
|
||||
expect(normalizePluginsConfig(config).slots.memory).toBe(expected);
|
||||
});
|
||||
|
||||
it("normalizes plugin hook policy flags", () => {
|
||||
@@ -172,36 +138,42 @@ describe("resolveEffectiveEnableState", () => {
|
||||
});
|
||||
}
|
||||
|
||||
it("enables bundled channels when channels.<id>.enabled=true", () => {
|
||||
const state = resolveBundledTelegramState({
|
||||
enabled: true,
|
||||
});
|
||||
expect(state).toEqual({ enabled: true });
|
||||
});
|
||||
|
||||
it("keeps explicit plugin-level disable authoritative", () => {
|
||||
const state = resolveBundledTelegramState({
|
||||
enabled: true,
|
||||
entries: {
|
||||
telegram: {
|
||||
enabled: false,
|
||||
it.each([
|
||||
[{ enabled: true }, { enabled: true }],
|
||||
[
|
||||
{
|
||||
enabled: true,
|
||||
entries: {
|
||||
telegram: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(state).toEqual({ enabled: false, reason: "disabled in config" });
|
||||
{ enabled: false, reason: "disabled in config" },
|
||||
],
|
||||
] as const)("resolves bundled telegram state for %o", (config, expected) => {
|
||||
expect(resolveBundledTelegramState(config)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveEnableState", () => {
|
||||
it("enables bundled plugins only when manifest metadata marks them enabled by default", () => {
|
||||
expect(resolveEnableState("openai", "bundled", normalizePluginsConfig({}))).toEqual({
|
||||
enabled: false,
|
||||
reason: "bundled (disabled by default)",
|
||||
});
|
||||
expect(resolveEnableState("openai", "bundled", normalizePluginsConfig({}), true)).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
it.each([
|
||||
[
|
||||
"openai",
|
||||
"bundled",
|
||||
normalizePluginsConfig({}),
|
||||
undefined,
|
||||
{ enabled: false, reason: "bundled (disabled by default)" },
|
||||
],
|
||||
["openai", "bundled", normalizePluginsConfig({}), true, { enabled: true }],
|
||||
["google", "bundled", normalizePluginsConfig({}), true, { enabled: true }],
|
||||
["profile-aware", "bundled", normalizePluginsConfig({}), true, { enabled: true }],
|
||||
] as const)(
|
||||
"resolves %s enable state for origin=%s manifestEnabledByDefault=%s",
|
||||
(id, origin, config, manifestEnabledByDefault, expected) => {
|
||||
expect(resolveEnableState(id, origin, config, manifestEnabledByDefault)).toEqual(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the selected memory slot plugin enabled even when omitted from plugins.allow", () => {
|
||||
const state = resolveEnableState(
|
||||
@@ -232,29 +204,21 @@ describe("resolveEnableState", () => {
|
||||
expect(state).toEqual({ enabled: false, reason: "disabled in config" });
|
||||
});
|
||||
|
||||
it("disables workspace plugins by default when they are only auto-discovered from the workspace", () => {
|
||||
const state = resolveEnableState("workspace-helper", "workspace", normalizePluginsConfig({}));
|
||||
expect(state).toEqual({
|
||||
enabled: false,
|
||||
reason: "workspace plugin (disabled by default)",
|
||||
});
|
||||
});
|
||||
|
||||
it("allows workspace plugins when explicitly listed in plugins.allow", () => {
|
||||
const state = resolveEnableState(
|
||||
"workspace-helper",
|
||||
"workspace",
|
||||
it.each([
|
||||
[
|
||||
normalizePluginsConfig({}),
|
||||
{
|
||||
enabled: false,
|
||||
reason: "workspace plugin (disabled by default)",
|
||||
},
|
||||
],
|
||||
[
|
||||
normalizePluginsConfig({
|
||||
allow: ["workspace-helper"],
|
||||
}),
|
||||
);
|
||||
expect(state).toEqual({ enabled: true });
|
||||
});
|
||||
|
||||
it("allows workspace plugins when explicitly enabled in plugin entries", () => {
|
||||
const state = resolveEnableState(
|
||||
"workspace-helper",
|
||||
"workspace",
|
||||
{ enabled: true },
|
||||
],
|
||||
[
|
||||
normalizePluginsConfig({
|
||||
entries: {
|
||||
"workspace-helper": {
|
||||
@@ -262,8 +226,10 @@ describe("resolveEnableState", () => {
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(state).toEqual({ enabled: true });
|
||||
{ enabled: true },
|
||||
],
|
||||
] as const)("resolves workspace-helper enable state for %o", (config, expected) => {
|
||||
expect(resolveEnableState("workspace-helper", "workspace", config)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("does not let the default memory slot auto-enable an untrusted workspace plugin", () => {
|
||||
@@ -279,14 +245,4 @@ describe("resolveEnableState", () => {
|
||||
reason: "workspace plugin (disabled by default)",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps bundled plugins enabled when manifest metadata marks them enabled by default", () => {
|
||||
const state = resolveEnableState("google", "bundled", normalizePluginsConfig({}), true);
|
||||
expect(state).toEqual({ enabled: true });
|
||||
});
|
||||
|
||||
it("allows bundled plugins to opt into default enablement from manifest metadata", () => {
|
||||
const state = resolveEnableState("profile-aware", "bundled", normalizePluginsConfig({}), true);
|
||||
expect(state).toEqual({ enabled: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,16 +111,18 @@ describe("provider auth-choice contract", () => {
|
||||
},
|
||||
];
|
||||
|
||||
for (const provider of pluginFallbackScenarios) {
|
||||
resolvePluginProvidersMock.mockClear();
|
||||
resolvePluginProvidersMock.mockReturnValue([provider]);
|
||||
await expect(
|
||||
resolvePreferredProviderForAuthChoice({
|
||||
choice: buildProviderPluginMethodChoice(provider.id, provider.auth[0]?.id ?? "default"),
|
||||
}),
|
||||
).resolves.toBe(provider.id);
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalled();
|
||||
}
|
||||
await Promise.all(
|
||||
pluginFallbackScenarios.map(async (provider) => {
|
||||
resolvePluginProvidersMock.mockClear();
|
||||
resolvePluginProvidersMock.mockReturnValue([provider]);
|
||||
await expect(
|
||||
resolvePreferredProviderForAuthChoice({
|
||||
choice: buildProviderPluginMethodChoice(provider.id, provider.auth[0]?.id ?? "default"),
|
||||
}),
|
||||
).resolves.toBe(provider.id);
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalled();
|
||||
}),
|
||||
);
|
||||
|
||||
resolvePluginProvidersMock.mockClear();
|
||||
await expect(resolvePreferredProviderForAuthChoice({ choice: "unknown" })).resolves.toBe(
|
||||
|
||||
@@ -15,16 +15,27 @@ function resolveBundledManifestProviderPluginIds() {
|
||||
);
|
||||
}
|
||||
|
||||
function expectPluginAllowlistContains(
|
||||
allow: string[] | undefined,
|
||||
pluginIds: string[],
|
||||
expectedExtraEntry?: string,
|
||||
) {
|
||||
expect(allow).toEqual(expect.arrayContaining(pluginIds));
|
||||
if (expectedExtraEntry) {
|
||||
expect(allow).toContain(expectedExtraEntry);
|
||||
}
|
||||
}
|
||||
|
||||
const demoAllowEntry = "demo-allowed";
|
||||
|
||||
describe("plugin loader contract", () => {
|
||||
let providerPluginIds: string[];
|
||||
let manifestProviderPluginIds: string[];
|
||||
let compatPluginIds: string[];
|
||||
let providerPluginIds: string[] = [];
|
||||
let manifestProviderPluginIds: string[] = [];
|
||||
let compatPluginIds: string[] = [];
|
||||
let compatConfig: ReturnType<typeof withBundledPluginAllowlistCompat>;
|
||||
let vitestCompatConfig: ReturnType<typeof providerTesting.withBundledProviderVitestCompat>;
|
||||
let webSearchPluginIds: string[];
|
||||
let bundledWebSearchPluginIds: string[];
|
||||
let webSearchPluginIds: string[] = [];
|
||||
let bundledWebSearchPluginIds: string[] = [];
|
||||
let webSearchAllowlistCompatConfig: ReturnType<typeof withBundledPluginAllowlistCompat>;
|
||||
|
||||
beforeAll(() => {
|
||||
@@ -72,24 +83,32 @@ describe("plugin loader contract", () => {
|
||||
expect(providerPluginIds).toEqual(manifestProviderPluginIds);
|
||||
expect(uniqueSortedStrings(compatPluginIds)).toEqual(manifestProviderPluginIds);
|
||||
expect(uniqueSortedStrings(compatPluginIds)).toEqual(expect.arrayContaining(providerPluginIds));
|
||||
expect(compatConfig?.plugins?.allow).toEqual(expect.arrayContaining(providerPluginIds));
|
||||
expectPluginAllowlistContains(compatConfig?.plugins?.allow, providerPluginIds, demoAllowEntry);
|
||||
});
|
||||
|
||||
it("keeps vitest bundled provider enablement wired to the provider registry", () => {
|
||||
expect(providerPluginIds).toEqual(manifestProviderPluginIds);
|
||||
expect(vitestCompatConfig?.plugins).toMatchObject({
|
||||
enabled: true,
|
||||
allow: expect.arrayContaining(providerPluginIds),
|
||||
});
|
||||
expect(vitestCompatConfig?.plugins?.enabled).toBe(true);
|
||||
expectPluginAllowlistContains(vitestCompatConfig?.plugins?.allow, providerPluginIds);
|
||||
});
|
||||
|
||||
it("keeps bundled web search loading scoped to the web search registry", () => {
|
||||
expect(bundledWebSearchPluginIds).toEqual(webSearchPluginIds);
|
||||
});
|
||||
|
||||
it("keeps bundled web search allowlist compatibility wired to the web search registry", () => {
|
||||
expect(webSearchAllowlistCompatConfig?.plugins?.allow).toEqual(
|
||||
expect.arrayContaining(webSearchPluginIds),
|
||||
);
|
||||
it.each([
|
||||
{
|
||||
name: "keeps bundled web search loading scoped to the web search registry",
|
||||
actual: bundledWebSearchPluginIds,
|
||||
expected: webSearchPluginIds,
|
||||
},
|
||||
{
|
||||
name: "keeps bundled web search allowlist compatibility wired to the web search registry",
|
||||
actual: webSearchAllowlistCompatConfig?.plugins?.allow,
|
||||
expected: webSearchPluginIds,
|
||||
extraEntry: demoAllowEntry,
|
||||
},
|
||||
] as const)("$name", ({ actual, expected, extraEntry }) => {
|
||||
if (Array.isArray(actual) && extraEntry == null) {
|
||||
expect(actual).toEqual(expected);
|
||||
return;
|
||||
}
|
||||
expectPluginAllowlistContains(actual, expected, extraEntry);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,45 +102,36 @@ const TEST_PROVIDER_IDS = TEST_PROVIDERS.map((provider) => provider.id).toSorted
|
||||
);
|
||||
|
||||
function resolveExpectedWizardChoiceValues(providers: ProviderPlugin[]) {
|
||||
const values: string[] = [];
|
||||
|
||||
for (const provider of providers) {
|
||||
const methodSetups = provider.auth.filter((method) => method.wizard);
|
||||
if (methodSetups.length > 0) {
|
||||
values.push(
|
||||
...methodSetups.map(
|
||||
return providers
|
||||
.flatMap((provider) => {
|
||||
const methodSetups = provider.auth.filter((method) => method.wizard);
|
||||
if (methodSetups.length > 0) {
|
||||
return methodSetups.map(
|
||||
(method) =>
|
||||
method.wizard?.choiceId?.trim() ||
|
||||
buildProviderPluginMethodChoice(provider.id, method.id),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const setup = provider.wizard?.setup;
|
||||
if (!setup) {
|
||||
continue;
|
||||
}
|
||||
const setup = provider.wizard?.setup;
|
||||
if (!setup) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const explicitMethodId = setup.methodId?.trim();
|
||||
if (explicitMethodId && provider.auth.some((method) => method.id === explicitMethodId)) {
|
||||
values.push(
|
||||
setup.choiceId?.trim() || buildProviderPluginMethodChoice(provider.id, explicitMethodId),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const explicitMethodId = setup.methodId?.trim();
|
||||
if (explicitMethodId && provider.auth.some((method) => method.id === explicitMethodId)) {
|
||||
return [
|
||||
setup.choiceId?.trim() || buildProviderPluginMethodChoice(provider.id, explicitMethodId),
|
||||
];
|
||||
}
|
||||
|
||||
if (provider.auth.length === 1) {
|
||||
values.push(setup.choiceId?.trim() || provider.id);
|
||||
continue;
|
||||
}
|
||||
if (provider.auth.length === 1) {
|
||||
return [setup.choiceId?.trim() || provider.id];
|
||||
}
|
||||
|
||||
values.push(
|
||||
...provider.auth.map((method) => buildProviderPluginMethodChoice(provider.id, method.id)),
|
||||
);
|
||||
}
|
||||
|
||||
return values.toSorted((left, right) => left.localeCompare(right));
|
||||
return provider.auth.map((method) => buildProviderPluginMethodChoice(provider.id, method.id));
|
||||
})
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function resolveExpectedModelPickerValues(providers: ProviderPlugin[]) {
|
||||
@@ -193,24 +184,16 @@ describe("provider wizard contract", () => {
|
||||
it("round-trips every shared wizard choice back to its provider and auth method", () => {
|
||||
const options = resolveProviderWizardOptions({ config: {}, env: process.env });
|
||||
|
||||
expect(options).toEqual(
|
||||
expect.arrayContaining(
|
||||
options.map((option) =>
|
||||
expect.objectContaining({
|
||||
value: option.value,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
for (const option of options) {
|
||||
const resolved = resolveProviderPluginChoice({
|
||||
providers: TEST_PROVIDERS,
|
||||
choice: option.value,
|
||||
});
|
||||
expect(resolved, option.value).not.toBeNull();
|
||||
expect(resolved?.provider.id, option.value).toBeTruthy();
|
||||
expect(resolved?.method.id, option.value).toBeTruthy();
|
||||
}
|
||||
expect(
|
||||
options.every((option) => {
|
||||
const resolved = resolveProviderPluginChoice({
|
||||
providers: TEST_PROVIDERS,
|
||||
choice: option.value,
|
||||
});
|
||||
return Boolean(resolved?.provider.id && resolved?.method.id);
|
||||
}),
|
||||
options.map((option) => option.value).join(", "),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("exposes every model-picker entry through the shared wizard layer", () => {
|
||||
@@ -219,12 +202,16 @@ describe("provider wizard contract", () => {
|
||||
expect(
|
||||
entries.map((entry) => entry.value).toSorted((left, right) => left.localeCompare(right)),
|
||||
).toEqual(resolveExpectedModelPickerValues(TEST_PROVIDERS));
|
||||
for (const entry of entries) {
|
||||
const resolved = resolveProviderPluginChoice({
|
||||
providers: TEST_PROVIDERS,
|
||||
choice: entry.value,
|
||||
});
|
||||
expect(resolved, entry.value).not.toBeNull();
|
||||
}
|
||||
expect(
|
||||
entries.every((entry) =>
|
||||
Boolean(
|
||||
resolveProviderPluginChoice({
|
||||
providers: TEST_PROVIDERS,
|
||||
choice: entry.value,
|
||||
}),
|
||||
),
|
||||
),
|
||||
entries.map((entry) => entry.value).join(", "),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,34 +2,40 @@ import { describe, expect, it } from "vitest";
|
||||
import { buildNpmResolutionInstallFields, recordPluginInstall } from "./installs.js";
|
||||
|
||||
describe("buildNpmResolutionInstallFields", () => {
|
||||
it("maps npm resolution metadata into install record fields", () => {
|
||||
const fields = buildNpmResolutionInstallFields({
|
||||
name: "@openclaw/demo",
|
||||
version: "1.2.3",
|
||||
resolvedSpec: "@openclaw/demo@1.2.3",
|
||||
integrity: "sha512-abc",
|
||||
shasum: "deadbeef",
|
||||
resolvedAt: "2026-02-22T00:00:00.000Z",
|
||||
});
|
||||
expect(fields).toEqual({
|
||||
resolvedName: "@openclaw/demo",
|
||||
resolvedVersion: "1.2.3",
|
||||
resolvedSpec: "@openclaw/demo@1.2.3",
|
||||
integrity: "sha512-abc",
|
||||
shasum: "deadbeef",
|
||||
resolvedAt: "2026-02-22T00:00:00.000Z",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined fields when resolution is missing", () => {
|
||||
expect(buildNpmResolutionInstallFields(undefined)).toEqual({
|
||||
resolvedName: undefined,
|
||||
resolvedVersion: undefined,
|
||||
resolvedSpec: undefined,
|
||||
integrity: undefined,
|
||||
shasum: undefined,
|
||||
resolvedAt: undefined,
|
||||
});
|
||||
it.each([
|
||||
{
|
||||
name: "maps npm resolution metadata into install record fields",
|
||||
input: {
|
||||
name: "@openclaw/demo",
|
||||
version: "1.2.3",
|
||||
resolvedSpec: "@openclaw/demo@1.2.3",
|
||||
integrity: "sha512-abc",
|
||||
shasum: "deadbeef",
|
||||
resolvedAt: "2026-02-22T00:00:00.000Z",
|
||||
},
|
||||
expected: {
|
||||
resolvedName: "@openclaw/demo",
|
||||
resolvedVersion: "1.2.3",
|
||||
resolvedSpec: "@openclaw/demo@1.2.3",
|
||||
integrity: "sha512-abc",
|
||||
shasum: "deadbeef",
|
||||
resolvedAt: "2026-02-22T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "returns undefined fields when resolution is missing",
|
||||
input: undefined,
|
||||
expected: {
|
||||
resolvedName: undefined,
|
||||
resolvedVersion: undefined,
|
||||
resolvedSpec: undefined,
|
||||
integrity: undefined,
|
||||
shasum: undefined,
|
||||
resolvedAt: undefined,
|
||||
},
|
||||
},
|
||||
] as const)("$name", ({ input, expected }) => {
|
||||
expect(buildNpmResolutionInstallFields(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,47 +18,57 @@ function createContext(models: ProviderRuntimeModel[]): ProviderResolveDynamicMo
|
||||
}
|
||||
|
||||
describe("cloneFirstTemplateModel", () => {
|
||||
it("clones the first matching template and applies patches", () => {
|
||||
const model = cloneFirstTemplateModel({
|
||||
providerId: "test-provider",
|
||||
modelId: " next-model ",
|
||||
templateIds: ["missing", "template-a", "template-b"],
|
||||
ctx: createContext([
|
||||
{
|
||||
id: "template-a",
|
||||
name: "Template A",
|
||||
provider: "test-provider",
|
||||
api: "openai-completions",
|
||||
} as ProviderRuntimeModel,
|
||||
]),
|
||||
patch: { reasoning: true },
|
||||
});
|
||||
|
||||
expect(model).toMatchObject({
|
||||
id: "next-model",
|
||||
name: "next-model",
|
||||
provider: "test-provider",
|
||||
api: "openai-completions",
|
||||
reasoning: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined when no template exists", () => {
|
||||
const model = cloneFirstTemplateModel({
|
||||
providerId: "test-provider",
|
||||
modelId: "next-model",
|
||||
templateIds: ["missing"],
|
||||
ctx: createContext([]),
|
||||
});
|
||||
|
||||
expect(model).toBeUndefined();
|
||||
it.each([
|
||||
{
|
||||
name: "clones the first matching template and applies patches",
|
||||
params: {
|
||||
providerId: "test-provider",
|
||||
modelId: " next-model ",
|
||||
templateIds: ["missing", "template-a", "template-b"],
|
||||
ctx: createContext([
|
||||
{
|
||||
id: "template-a",
|
||||
name: "Template A",
|
||||
provider: "test-provider",
|
||||
api: "openai-completions",
|
||||
} as ProviderRuntimeModel,
|
||||
]),
|
||||
patch: { reasoning: true },
|
||||
},
|
||||
expected: {
|
||||
id: "next-model",
|
||||
name: "next-model",
|
||||
provider: "test-provider",
|
||||
api: "openai-completions",
|
||||
reasoning: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "returns undefined when no template exists",
|
||||
params: {
|
||||
providerId: "test-provider",
|
||||
modelId: "next-model",
|
||||
templateIds: ["missing"],
|
||||
ctx: createContext([]),
|
||||
},
|
||||
expected: undefined,
|
||||
},
|
||||
] as const)("$name", ({ params, expected }) => {
|
||||
const model = cloneFirstTemplateModel(params);
|
||||
if (expected == null) {
|
||||
expect(model).toBeUndefined();
|
||||
return;
|
||||
}
|
||||
expect(model).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matchesExactOrPrefix", () => {
|
||||
it("matches exact ids and prefixed variants case-insensitively", () => {
|
||||
expect(matchesExactOrPrefix("MiniMax-M2.7", ["minimax-m2.7"])).toBe(true);
|
||||
expect(matchesExactOrPrefix("minimax-m2.7-highspeed", ["MiniMax-M2.7"])).toBe(true);
|
||||
expect(matchesExactOrPrefix("glm-5", ["minimax-m2.7"])).toBe(false);
|
||||
it.each([
|
||||
["MiniMax-M2.7", ["minimax-m2.7"], true],
|
||||
["minimax-m2.7-highspeed", ["MiniMax-M2.7"], true],
|
||||
["glm-5", ["minimax-m2.7"], false],
|
||||
] as const)("matches %s against prefixes", (id, candidates, expected) => {
|
||||
expect(matchesExactOrPrefix(id, candidates)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,6 +43,41 @@ function expectServiceContext(
|
||||
expect(typeof ctx.logger.error).toBe("function");
|
||||
}
|
||||
|
||||
function createTrackingService(
|
||||
id: string,
|
||||
params: {
|
||||
starts?: string[];
|
||||
stops?: string[];
|
||||
contexts?: OpenClawPluginServiceContext[];
|
||||
failOnStart?: boolean;
|
||||
failOnStop?: boolean;
|
||||
stopSpy?: () => void;
|
||||
} = {},
|
||||
): OpenClawPluginService {
|
||||
return {
|
||||
id,
|
||||
start: (ctx) => {
|
||||
if (params.failOnStart) {
|
||||
throw new Error("start failed");
|
||||
}
|
||||
params.starts?.push(id.at(-1) ?? id);
|
||||
params.contexts?.push(ctx);
|
||||
},
|
||||
stop: params.stopSpy
|
||||
? () => {
|
||||
params.stopSpy?.();
|
||||
}
|
||||
: params.stops || params.failOnStop
|
||||
? () => {
|
||||
if (params.failOnStop) {
|
||||
throw new Error("stop failed");
|
||||
}
|
||||
params.stops?.push(id.at(-1) ?? id);
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
describe("startPluginServices", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -53,37 +88,13 @@ describe("startPluginServices", () => {
|
||||
const stops: string[] = [];
|
||||
const contexts: OpenClawPluginServiceContext[] = [];
|
||||
|
||||
const serviceA: OpenClawPluginService = {
|
||||
id: "service-a",
|
||||
start: (ctx) => {
|
||||
starts.push("a");
|
||||
contexts.push(ctx);
|
||||
},
|
||||
stop: () => {
|
||||
stops.push("a");
|
||||
},
|
||||
};
|
||||
const serviceB: OpenClawPluginService = {
|
||||
id: "service-b",
|
||||
start: (ctx) => {
|
||||
starts.push("b");
|
||||
contexts.push(ctx);
|
||||
},
|
||||
};
|
||||
const serviceC: OpenClawPluginService = {
|
||||
id: "service-c",
|
||||
start: (ctx) => {
|
||||
starts.push("c");
|
||||
contexts.push(ctx);
|
||||
},
|
||||
stop: () => {
|
||||
stops.push("c");
|
||||
},
|
||||
};
|
||||
|
||||
const config = {} as Parameters<typeof startPluginServices>[0]["config"];
|
||||
const handle = await startPluginServices({
|
||||
registry: createRegistry([serviceA, serviceB, serviceC]),
|
||||
registry: createRegistry([
|
||||
createTrackingService("service-a", { starts, stops, contexts }),
|
||||
createTrackingService("service-b", { starts, contexts }),
|
||||
createTrackingService("service-c", { starts, stops, contexts }),
|
||||
]),
|
||||
config,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
@@ -105,23 +116,12 @@ describe("startPluginServices", () => {
|
||||
|
||||
const handle = await startPluginServices({
|
||||
registry: createRegistry([
|
||||
{
|
||||
id: "service-start-fail",
|
||||
start: () => {
|
||||
throw new Error("start failed");
|
||||
},
|
||||
stop: vi.fn(),
|
||||
},
|
||||
{
|
||||
id: "service-ok",
|
||||
start: () => undefined,
|
||||
stop: stopOk,
|
||||
},
|
||||
{
|
||||
id: "service-stop-fail",
|
||||
start: () => undefined,
|
||||
stop: stopThrows,
|
||||
},
|
||||
createTrackingService("service-start-fail", {
|
||||
failOnStart: true,
|
||||
stopSpy: vi.fn(),
|
||||
}),
|
||||
createTrackingService("service-ok", { stopSpy: stopOk }),
|
||||
createTrackingService("service-stop-fail", { stopSpy: stopThrows }),
|
||||
]),
|
||||
config: {} as Parameters<typeof startPluginServices>[0]["config"],
|
||||
});
|
||||
|
||||
@@ -29,6 +29,21 @@ describe("applyExclusiveSlotSelection", () => {
|
||||
},
|
||||
});
|
||||
|
||||
function expectSelectionWarnings(
|
||||
warnings: string[],
|
||||
params: {
|
||||
contains?: readonly string[];
|
||||
excludes?: readonly string[];
|
||||
},
|
||||
) {
|
||||
for (const warning of params.contains ?? []) {
|
||||
expect(warnings).toContain(warning);
|
||||
}
|
||||
for (const warning of params.excludes ?? []) {
|
||||
expect(warnings).not.toContain(warning);
|
||||
}
|
||||
}
|
||||
|
||||
it("selects the slot and disables other entries for the same kind", () => {
|
||||
const config = createMemoryConfig({
|
||||
slots: { memory: "memory-core" },
|
||||
@@ -61,35 +76,38 @@ describe("applyExclusiveSlotSelection", () => {
|
||||
expect(result.config).toBe(config);
|
||||
});
|
||||
|
||||
it("warns when the slot falls back to a default", () => {
|
||||
const config = createMemoryConfig();
|
||||
const result = applyExclusiveSlotSelection({
|
||||
config,
|
||||
it.each([
|
||||
{
|
||||
name: "warns when the slot falls back to a default",
|
||||
config: createMemoryConfig(),
|
||||
selectedId: "memory",
|
||||
selectedKind: "memory",
|
||||
registry: { plugins: [{ id: "memory", kind: "memory" }] },
|
||||
});
|
||||
|
||||
expect(result.changed).toBe(true);
|
||||
expect(result.warnings).toContain(
|
||||
'Exclusive slot "memory" switched from "memory-core" to "memory".',
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps disabled competing plugins disabled without adding disable warnings", () => {
|
||||
const config = createMemoryConfig({
|
||||
entries: {
|
||||
"memory-core": { enabled: false },
|
||||
expectedDisabled: undefined,
|
||||
warningChecks: {
|
||||
contains: ['Exclusive slot "memory" switched from "memory-core" to "memory".'],
|
||||
},
|
||||
});
|
||||
const result = runMemorySelection(config);
|
||||
},
|
||||
{
|
||||
name: "keeps disabled competing plugins disabled without adding disable warnings",
|
||||
config: createMemoryConfig({
|
||||
entries: {
|
||||
"memory-core": { enabled: false },
|
||||
},
|
||||
}),
|
||||
selectedId: "memory",
|
||||
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, selectedId, expectedDisabled, warningChecks }) => {
|
||||
const result = runMemorySelection(config, selectedId);
|
||||
|
||||
expect(result.changed).toBe(true);
|
||||
expect(result.config.plugins?.entries?.["memory-core"]?.enabled).toBe(false);
|
||||
expect(result.warnings).toContain(
|
||||
'Exclusive slot "memory" switched from "memory-core" to "memory".',
|
||||
);
|
||||
expect(result.warnings).not.toContain('Disabled other "memory" slot plugins: memory-core.');
|
||||
if (expectedDisabled != null) {
|
||||
expect(result.config.plugins?.entries?.["memory-core"]?.enabled).toBe(expectedDisabled);
|
||||
}
|
||||
expectSelectionWarnings(result.warnings, warningChecks);
|
||||
});
|
||||
|
||||
it("skips changes when no exclusive slot applies", () => {
|
||||
|
||||
Reference in New Issue
Block a user