diff --git a/src/plugins/manifest.json5-tolerance.test.ts b/src/plugins/manifest.json5-tolerance.test.ts index d46df8ad821b..5e3f74a62251 100644 --- a/src/plugins/manifest.json5-tolerance.test.ts +++ b/src/plugins/manifest.json5-tolerance.test.ts @@ -3,6 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import JSON5 from "json5"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { resolveMemorySlotDecision } from "./config-state.js"; import { loadPluginManifest } from "./manifest.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js"; @@ -122,6 +123,100 @@ describe("loadPluginManifest JSON5 tolerance", () => { } }); + it.each([ + { + name: "a supported memory kind", + rawKind: "memory", + expectedKind: "memory", + }, + { + name: "a supported context-engine kind", + rawKind: "context-engine", + expectedKind: "context-engine", + }, + { + name: "both supported kinds in declaration order", + rawKind: ["context-engine", "memory"], + expectedKind: ["context-engine", "memory"], + }, + { + name: "duplicate memory kinds collapsed into one kind", + rawKind: ["memory", "memory"], + expectedKind: "memory", + }, + { + name: "duplicate context-engine kinds collapsed into one kind", + rawKind: ["context-engine", "context-engine"], + expectedKind: "context-engine", + }, + { + name: "supported kinds filtered from invalid and duplicate array entries", + rawKind: ["memory", "unknown-kind", 42, "memory", "context-engine", null], + expectedKind: ["memory", "context-engine"], + }, + { + name: "a valid memory kind retained alongside invalid entries", + rawKind: ["unknown-kind", 42, "memory", null], + expectedKind: "memory", + }, + { + name: "an unsupported scalar kind", + rawKind: "unknown-kind", + expectedKind: undefined, + }, + { + name: "an array containing only unsupported kinds", + rawKind: ["unknown-kind", 42, null], + expectedKind: undefined, + }, + ])("normalizes $name", ({ rawKind, expectedKind }) => { + const dir = makeTempDir(); + fs.writeFileSync( + path.join(dir, "openclaw.plugin.json"), + JSON.stringify({ + id: "kind-normalization", + kind: rawKind, + configSchema: { type: "object" }, + }), + "utf-8", + ); + + const result = loadPluginManifest(dir, false); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.manifest.kind).toEqual(expectedKind); + } + }); + + it("keeps duplicate memory declarations subject to the exclusive memory slot", () => { + const dir = makeTempDir(); + fs.writeFileSync( + path.join(dir, "openclaw.plugin.json"), + JSON.stringify({ + id: "duplicate-memory", + kind: ["memory", "memory"], + configSchema: { type: "object" }, + }), + "utf-8", + ); + + const result = loadPluginManifest(dir, false); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.manifest.kind).toBe("memory"); + expect( + resolveMemorySlotDecision({ + id: result.manifest.id, + kind: result.manifest.kind, + slot: "memory-core", + selectedId: "memory-core", + }), + ).toEqual({ enabled: false, reason: 'memory slot set to "memory-core"' }); + } + }); + it("normalizes modelSupport metadata from the manifest", () => { const dir = makeTempDir(); const json5Content = `{ diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index 46748dd37d63..0b8d259c3748 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -29,6 +29,7 @@ const PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME] as const; const MAX_PLUGIN_MANIFEST_BYTES = 256 * 1024; const MAX_PLUGIN_MANIFEST_LOAD_CACHE_ENTRIES = 512; const CORE_RESERVED_PLUGIN_IDS = new Set(["node-mcp"]); +const VALID_PLUGIN_KINDS: ReadonlySet = new Set(["memory", "context-engine"]); export function isCoreReservedPluginId(id: string): boolean { return CORE_RESERVED_PLUGIN_IDS.has(normalizePluginPolicyId(id)); @@ -109,13 +110,18 @@ function setCachedPluginManifestLoadResult( } function parsePluginKind(raw: unknown): PluginKind | PluginKind[] | undefined { - if (typeof raw === "string") { - return raw as PluginKind; + const values = typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : []; + const kinds: PluginKind[] = []; + for (const value of values) { + if (typeof value !== "string" || !VALID_PLUGIN_KINDS.has(value)) { + continue; + } + const kind = value as PluginKind; + if (!kinds.includes(kind)) { + kinds.push(kind); + } } - if (Array.isArray(raw) && raw.length > 0 && raw.every((k) => typeof k === "string")) { - return raw.length === 1 ? (raw[0] as PluginKind) : (raw as PluginKind[]); - } - return undefined; + return kinds.length === 0 ? undefined : kinds.length === 1 ? kinds[0] : kinds; } export function loadPluginManifest(