


























@@ -119,9 +119,63 @@ function expectRegistryDiagnosticContains(
119119registry: ReturnType<typeof loadPluginManifestRegistry>,
120120fragment: string,
121121) {
122-expect(registry.diagnostics.map((diag) => diag.message)).toEqual(
123-expect.arrayContaining([expect.stringContaining(fragment)]),
124-);
122+expect(registry.diagnostics.some((diag) => diag.message.includes(fragment))).toBe(true);
123+}
124+125+function expectNoRegistryDiagnosticContains(
126+registry: ReturnType<typeof loadPluginManifestRegistry>,
127+fragment: string,
128+) {
129+expect(registry.diagnostics.some((diag) => diag.message.includes(fragment))).toBe(false);
130+}
131+132+function requireRecord(value: unknown, label: string): Record<string, unknown> {
133+expect(
134+typeof value === "object" && value !== null && !Array.isArray(value),
135+`${label} object`,
136+).toBe(true);
137+return value as Record<string, unknown>;
138+}
139+140+function expectRecordFields(
141+value: unknown,
142+label: string,
143+expected: Record<string, unknown>,
144+): Record<string, unknown> {
145+const record = requireRecord(value, label);
146+for (const [key, expectedValue] of Object.entries(expected)) {
147+expect(record[key], `${label}.${key}`).toEqual(expectedValue);
148+}
149+return record;
150+}
151+152+function expectArrayIncludesAll(value: unknown, expected: readonly unknown[], label: string) {
153+expect(Array.isArray(value), `${label} array`).toBe(true);
154+for (const item of expected) {
155+expect(value as unknown[], `${label} item ${String(item)}`).toContain(item);
156+}
157+}
158+159+function expectDiagnosticFields(
160+registry: ReturnType<typeof loadPluginManifestRegistry>,
161+expected: { level?: string; pluginId?: string; source?: string; messageIncludes?: string },
162+) {
163+const diagnostic = registry.diagnostics.find((entry) => {
164+if (expected.level && entry.level !== expected.level) {
165+return false;
166+}
167+if (expected.pluginId && entry.pluginId !== expected.pluginId) {
168+return false;
169+}
170+if (expected.source && entry.source !== expected.source) {
171+return false;
172+}
173+if (expected.messageIncludes && !entry.message.includes(expected.messageIncludes)) {
174+return false;
175+}
176+return true;
177+});
178+expect(diagnostic, `diagnostic ${expected.messageIncludes ?? ""}`).toBeDefined();
125179}
126180127181function prepareLinkedManifestFixture(params: { id: string; mode: "symlink" | "hardlink" }): {
@@ -455,17 +509,13 @@ describe("loadPluginManifestRegistry", () => {
455509config: { plugins: { entries: { "external-chat": { enabled: false } } } },
456510candidates: [candidate],
457511});
458-expect(disabledRegistry.diagnostics.map((diagnostic) => diagnostic.message)).not.toEqual(
459-expect.arrayContaining([expect.stringContaining("without channelConfigs metadata")]),
460-);
512+expectNoRegistryDiagnosticContains(disabledRegistry, "without channelConfigs metadata");
461513462514const allowlistRegistry = loadPluginManifestRegistry({
463515config: { plugins: { allow: ["other-plugin"] } },
464516candidates: [candidate],
465517});
466-expect(allowlistRegistry.diagnostics.map((diagnostic) => diagnostic.message)).not.toEqual(
467-expect.arrayContaining([expect.stringContaining("without channelConfigs metadata")]),
468-);
518+expectNoRegistryDiagnosticContains(allowlistRegistry, "without channelConfigs metadata");
469519});
470520471521it("suppresses duplicate warnings for explicit installed globals overriding bundled plugins", () => {
@@ -590,12 +640,10 @@ describe("loadPluginManifestRegistry", () => {
590640});
591641592642expect(registry.plugins).toHaveLength(1);
593-expect(registry.plugins[0]).toEqual(
594-expect.objectContaining({
595-origin: "config",
596-trustedOfficialInstall: true,
597-}),
598-);
643+expectRecordFields(registry.plugins[0], "plugin", {
644+origin: "config",
645+trustedOfficialInstall: true,
646+});
599647});
600648601649it("does not trust unrecorded globals that spoof official ids", () => {
@@ -1064,16 +1112,12 @@ describe("loadPluginManifestRegistry", () => {
10641112expect(registry.plugins[0]?.providerAuthEnvVars).toEqual({
10651113openai: ["OPENAI_API_KEY"],
10661114});
1067-expect(registry.diagnostics).toContainEqual(
1068-expect.objectContaining({
1069-level: "warn",
1070-pluginId: "external-openai",
1071-source: path.join(dir, "openclaw.plugin.json"),
1072-message: expect.stringContaining(
1073-"providerAuthEnvVars is deprecated compatibility metadata",
1074-),
1075-}),
1076-);
1115+expectDiagnosticFields(registry, {
1116+level: "warn",
1117+pluginId: "external-openai",
1118+source: path.join(dir, "openclaw.plugin.json"),
1119+messageIncludes: "providerAuthEnvVars is deprecated compatibility metadata",
1120+});
10771121});
1078112210791123it("does not report deprecated providerAuthEnvVars when setup providers mirror env vars", () => {
@@ -1096,12 +1140,9 @@ describe("loadPluginManifestRegistry", () => {
10961140origin: "global",
10971141});
109811421099-expect(registry.diagnostics).not.toContainEqual(
1100-expect.objectContaining({
1101-message: expect.stringContaining(
1102-"providerAuthEnvVars is deprecated compatibility metadata",
1103-),
1104-}),
1143+expectNoRegistryDiagnosticContains(
1144+registry,
1145+"providerAuthEnvVars is deprecated compatibility metadata",
11051146);
11061147});
11071148@@ -1148,14 +1189,12 @@ describe("loadPluginManifestRegistry", () => {
11481189});
1149119011501191expect(registry.plugins[0]?.channels).toEqual(["external-chat"]);
1151-expect(registry.diagnostics).toContainEqual(
1152-expect.objectContaining({
1153-level: "warn",
1154-pluginId: "external-chat",
1155-source: path.join(dir, "openclaw.plugin.json"),
1156-message: expect.stringContaining("without channelConfigs metadata"),
1157-}),
1158-);
1192+expectDiagnosticFields(registry, {
1193+level: "warn",
1194+pluginId: "external-chat",
1195+source: path.join(dir, "openclaw.plugin.json"),
1196+messageIncludes: "without channelConfigs metadata",
1197+});
11591198});
1160119911611200it("sanitizes manifest-controlled fields in channel config descriptor diagnostics", () => {
@@ -1208,13 +1247,11 @@ describe("loadPluginManifestRegistry", () => {
12081247origin: "global",
12091248});
121012491211-expect(registry.plugins[0]?.channelConfigs?.["external-chat"]?.schema).toMatchObject({
1250+expectRecordFields(registry.plugins[0]?.channelConfigs?.["external-chat"]?.schema, "schema", {
12121251type: "object",
12131252additionalProperties: false,
12141253});
1215-expect(registry.diagnostics.map((diagnostic) => diagnostic.message)).not.toEqual(
1216-expect.arrayContaining([expect.stringContaining("without channelConfigs metadata")]),
1217-);
1254+expectNoRegistryDiagnosticContains(registry, "without channelConfigs metadata");
12181255});
1219125612201257it("hydrates supplemental official external catalog contracts for lagging npm manifests", () => {
@@ -1235,17 +1272,15 @@ describe("loadPluginManifestRegistry", () => {
12351272]);
1236127312371274expect(registry.plugins[0]?.contracts?.tools).toEqual(["wecom_mcp"]);
1238-expect(registry.plugins[0]?.channelConfigs?.wecom).toEqual(
1239-expect.objectContaining({
1275+const wecomConfig = expectRecordFields(
1276+registry.plugins[0]?.channelConfigs?.wecom,
1277+"wecom config",
1278+{
12401279label: "WeCom",
1241-schema: expect.objectContaining({
1242-type: "object",
1243-}),
1244-}),
1245-);
1246-expect(registry.diagnostics.map((diagnostic) => diagnostic.message)).not.toEqual(
1247-expect.arrayContaining([expect.stringContaining("without channelConfigs metadata")]),
1280+},
12481281);
1282+expectRecordFields(wecomConfig.schema, "wecom schema", { type: "object" });
1283+expectNoRegistryDiagnosticContains(registry, "without channelConfigs metadata");
12491284});
1250128512511286it("fills missing official external catalog descriptors for partial npm channel configs", () => {
@@ -1276,18 +1311,20 @@ describe("loadPluginManifestRegistry", () => {
12761311}),
12771312]);
127813131279-expect(registry.plugins[0]?.channelConfigs?.wecom).toEqual(
1280-expect.objectContaining({
1314+const wecomConfig = expectRecordFields(
1315+registry.plugins[0]?.channelConfigs?.wecom,
1316+"wecom config",
1317+{
12811318label: "WeCom",
12821319description: "Enterprise WeChat conversation channel.",
1283-schema: expect.objectContaining({
1284-additionalProperties: false,
1285-properties: {
1286-corpId: { type: "string" },
1287-},
1288-}),
1289-}),
1320+},
12901321);
1322+expectRecordFields(wecomConfig.schema, "wecom schema", {
1323+additionalProperties: false,
1324+properties: {
1325+corpId: { type: "string" },
1326+},
1327+});
12911328});
1292132912931330it("drops prototype-polluting channel config keys from plugin manifests", () => {
@@ -1338,7 +1375,7 @@ describe("loadPluginManifestRegistry", () => {
13381375expect(Object.prototype.hasOwnProperty.call(channelConfigs, "__proto__")).toBe(false);
13391376expect(Object.prototype.hasOwnProperty.call(channelConfigs, "constructor")).toBe(false);
13401377expect(Object.prototype.hasOwnProperty.call(channelConfigs, "prototype")).toBe(false);
1341-expect(channelConfigs["safe-chat"]?.schema).toMatchObject({
1378+expectRecordFields(channelConfigs["safe-chat"]?.schema, "safe-chat schema", {
13421379type: "object",
13431380additionalProperties: false,
13441381});
@@ -1754,13 +1791,11 @@ describe("loadPluginManifestRegistry", () => {
17541791}),
17551792]);
175617931757-expect(registry.plugins[0]?.channelConfigs?.telegram).toEqual(
1758-expect.objectContaining({
1759-schema: expect.objectContaining({
1760-type: "object",
1761-}),
1762-}),
1794+const telegramConfig = requireRecord(
1795+registry.plugins[0]?.channelConfigs?.telegram,
1796+"telegram config",
17631797);
1798+expectRecordFields(telegramConfig.schema, "telegram schema", { type: "object" });
17641799});
1765180017661801it("preserves manifest-owned config contracts from plugin manifests", () => {
@@ -1935,9 +1970,7 @@ describe("loadPluginManifestRegistry", () => {
19351970});
1936197119371972expect(registry.plugins.map((plugin) => plugin.id)).toEqual(["codex"]);
1938-expect(registry.diagnostics.map((diag) => diag.message)).not.toEqual(
1939-expect.arrayContaining([expect.stringContaining("openclaw.install.minHostVersion must use")]),
1940-);
1973+expectNoRegistryDiagnosticContains(registry, "openclaw.install.minHostVersion must use");
19411974});
1942197519431976it("does not runtime-gate bundled source plugins by install minHostVersion", () => {
@@ -1963,9 +1996,7 @@ describe("loadPluginManifestRegistry", () => {
19631996});
1964199719651998expect(registry.plugins.map((plugin) => plugin.id)).toContain("codex");
1966-expect(registry.diagnostics.map((diag) => diag.message)).not.toEqual(
1967-expect.arrayContaining([expect.stringContaining("requires OpenClaw")]),
1968-);
1999+expectNoRegistryDiagnosticContains(registry, "requires OpenClaw");
19692000});
1970200119712002it.each([
@@ -2115,8 +2146,8 @@ describe("loadPluginManifestRegistry", () => {
21152146bundleFormat: "codex",
21162147hooks: ["hooks"],
21172148skills: ["skills"],
2118-bundleCapabilities: expect.arrayContaining(["hooks", "skills"]),
21192149},
2150+expectedCapabilities: ["hooks", "skills"],
21202151},
21212152{
21222153name: "loads Claude bundle manifests with command roots and settings files",
@@ -2143,8 +2174,8 @@ describe("loadPluginManifestRegistry", () => {
21432174bundleFormat: "claude",
21442175skills: ["skill-packs/starter", "commands-pack"],
21452176settingsFiles: ["settings.json"],
2146-bundleCapabilities: expect.arrayContaining(["skills", "commands", "settings"]),
21472177},
2178+expectedCapabilities: ["skills", "commands", "settings"],
21482179},
21492180{
21502181name: "loads manifestless Claude bundles into the registry",
@@ -2164,8 +2195,8 @@ describe("loadPluginManifestRegistry", () => {
21642195bundleFormat: "claude",
21652196skills: ["commands"],
21662197settingsFiles: ["settings.json"],
2167-bundleCapabilities: expect.arrayContaining(["skills", "commands", "settings"]),
21682198},
2199+expectedCapabilities: ["skills", "commands", "settings"],
21692200},
21702201{
21712202name: "loads Cursor bundle manifests into the registry",
@@ -2191,24 +2222,23 @@ describe("loadPluginManifestRegistry", () => {
21912222format: "bundle",
21922223bundleFormat: "cursor",
21932224skills: ["skills", ".cursor/commands"],
2194-bundleCapabilities: expect.arrayContaining([
2195-"skills",
2196-"commands",
2197-"rules",
2198-"hooks",
2199-"mcpServers",
2200-]),
22012225},
2226+expectedCapabilities: ["skills", "commands", "rules", "hooks", "mcpServers"],
22022227},
2203-] as const)("$name", ({ idHint, bundleFormat, setup, expected }) => {
2228+] as const)("$name", ({ idHint, bundleFormat, setup, expected, expectedCapabilities }) => {
22042229const registry = loadBundleRegistry({
22052230 idHint,
22062231 bundleFormat,
22072232 setup,
22082233});
2209223422102235expect(registry.plugins).toHaveLength(1);
2211-expect(registry.plugins[0]).toMatchObject(expected);
2236+expectRecordFields(registry.plugins[0], "bundle plugin", expected);
2237+expectArrayIncludesAll(
2238+registry.plugins[0]?.bundleCapabilities,
2239+expectedCapabilities,
2240+"bundle capabilities",
2241+);
22122242});
2213224322142244it("prefers higher-precedence origins for the same physical directory (config > workspace > global > bundled)", () => {
@@ -2376,12 +2406,8 @@ describe("loadPluginManifestRegistry", () => {
23762406});
2377240723782408expect(olderHost.plugins).toStrictEqual([]);
2379-expect(olderHost.diagnostics.map((diag) => diag.message)).toEqual(
2380-expect.arrayContaining([expect.stringContaining("this host is 2026.3.21")]),
2381-);
2409+expectRegistryDiagnosticContains(olderHost, "this host is 2026.3.21");
23822410expect(newerHost.plugins.map((plugin) => plugin.id)).toContain("synology-chat");
2383-expect(newerHost.diagnostics.map((diag) => diag.message)).not.toEqual(
2384-expect.arrayContaining([expect.stringContaining("this host is 2026.3.21")]),
2385-);
2411+expectNoRegistryDiagnosticContains(newerHost, "this host is 2026.3.21");
23862412});
23872413});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。