From fad42b19eeaeb8c2e72b0a086a118cd18e850d9b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 28 Mar 2026 01:37:20 +0000 Subject: [PATCH] test: dedupe plugin core utility suites --- src/plugins/config-state.test.ts | 156 +++++++----------- .../contracts/auth-choice.contract.test.ts | 22 +-- src/plugins/contracts/loader.contract.test.ts | 55 ++++-- src/plugins/contracts/wizard.contract.test.ts | 101 +++++------- src/plugins/installs.test.ts | 62 +++---- src/plugins/provider-model-helpers.test.ts | 86 +++++----- src/plugins/services.test.ts | 92 +++++------ src/plugins/slots.test.ts | 68 +++++--- 8 files changed, 320 insertions(+), 322 deletions(-) diff --git a/src/plugins/config-state.test.ts b/src/plugins/config-state.test.ts index 5565f9653d3..2ff3e0862e5 100644 --- a/src/plugins/config-state.test.ts +++ b/src/plugins/config-state.test.ts @@ -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..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 }); - }); }); diff --git a/src/plugins/contracts/auth-choice.contract.test.ts b/src/plugins/contracts/auth-choice.contract.test.ts index c7eac968c2b..c71886b7e96 100644 --- a/src/plugins/contracts/auth-choice.contract.test.ts +++ b/src/plugins/contracts/auth-choice.contract.test.ts @@ -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( diff --git a/src/plugins/contracts/loader.contract.test.ts b/src/plugins/contracts/loader.contract.test.ts index a8bba1e0b22..ed449964870 100644 --- a/src/plugins/contracts/loader.contract.test.ts +++ b/src/plugins/contracts/loader.contract.test.ts @@ -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; let vitestCompatConfig: ReturnType; - let webSearchPluginIds: string[]; - let bundledWebSearchPluginIds: string[]; + let webSearchPluginIds: string[] = []; + let bundledWebSearchPluginIds: string[] = []; let webSearchAllowlistCompatConfig: ReturnType; 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); }); }); diff --git a/src/plugins/contracts/wizard.contract.test.ts b/src/plugins/contracts/wizard.contract.test.ts index 4bf3c7c8f58..425e095fa69 100644 --- a/src/plugins/contracts/wizard.contract.test.ts +++ b/src/plugins/contracts/wizard.contract.test.ts @@ -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); }); }); diff --git a/src/plugins/installs.test.ts b/src/plugins/installs.test.ts index 0a3d785b4e9..c95329cd215 100644 --- a/src/plugins/installs.test.ts +++ b/src/plugins/installs.test.ts @@ -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); }); }); diff --git a/src/plugins/provider-model-helpers.test.ts b/src/plugins/provider-model-helpers.test.ts index 4adf16d589d..0fc1d7384d7 100644 --- a/src/plugins/provider-model-helpers.test.ts +++ b/src/plugins/provider-model-helpers.test.ts @@ -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); }); }); diff --git a/src/plugins/services.test.ts b/src/plugins/services.test.ts index 9ae4d50f181..4bb3a901907 100644 --- a/src/plugins/services.test.ts +++ b/src/plugins/services.test.ts @@ -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[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[0]["config"], }); diff --git a/src/plugins/slots.test.ts b/src/plugins/slots.test.ts index 56f18e039f8..42e9148eedc 100644 --- a/src/plugins/slots.test.ts +++ b/src/plugins/slots.test.ts @@ -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", () => {