test: clear manifest registry broad matchers

This commit is contained in:
Peter Steinberger
2026-05-10 11:57:13 +01:00
parent df1c9ffc2e
commit ec482c7564

View File

@@ -119,9 +119,63 @@ function expectRegistryDiagnosticContains(
registry: ReturnType<typeof loadPluginManifestRegistry>,
fragment: string,
) {
expect(registry.diagnostics.map((diag) => diag.message)).toEqual(
expect.arrayContaining([expect.stringContaining(fragment)]),
);
expect(registry.diagnostics.some((diag) => diag.message.includes(fragment))).toBe(true);
}
function expectNoRegistryDiagnosticContains(
registry: ReturnType<typeof loadPluginManifestRegistry>,
fragment: string,
) {
expect(registry.diagnostics.some((diag) => diag.message.includes(fragment))).toBe(false);
}
function requireRecord(value: unknown, label: string): Record<string, unknown> {
expect(
typeof value === "object" && value !== null && !Array.isArray(value),
`${label} object`,
).toBe(true);
return value as Record<string, unknown>;
}
function expectRecordFields(
value: unknown,
label: string,
expected: Record<string, unknown>,
): Record<string, unknown> {
const record = requireRecord(value, label);
for (const [key, expectedValue] of Object.entries(expected)) {
expect(record[key], `${label}.${key}`).toEqual(expectedValue);
}
return record;
}
function expectArrayIncludesAll(value: unknown, expected: readonly unknown[], label: string) {
expect(Array.isArray(value), `${label} array`).toBe(true);
for (const item of expected) {
expect(value as unknown[], `${label} item ${String(item)}`).toContain(item);
}
}
function expectDiagnosticFields(
registry: ReturnType<typeof loadPluginManifestRegistry>,
expected: { level?: string; pluginId?: string; source?: string; messageIncludes?: string },
) {
const diagnostic = registry.diagnostics.find((entry) => {
if (expected.level && entry.level !== expected.level) {
return false;
}
if (expected.pluginId && entry.pluginId !== expected.pluginId) {
return false;
}
if (expected.source && entry.source !== expected.source) {
return false;
}
if (expected.messageIncludes && !entry.message.includes(expected.messageIncludes)) {
return false;
}
return true;
});
expect(diagnostic, `diagnostic ${expected.messageIncludes ?? ""}`).toBeDefined();
}
function prepareLinkedManifestFixture(params: { id: string; mode: "symlink" | "hardlink" }): {
@@ -455,17 +509,13 @@ describe("loadPluginManifestRegistry", () => {
config: { plugins: { entries: { "external-chat": { enabled: false } } } },
candidates: [candidate],
});
expect(disabledRegistry.diagnostics.map((diagnostic) => diagnostic.message)).not.toEqual(
expect.arrayContaining([expect.stringContaining("without channelConfigs metadata")]),
);
expectNoRegistryDiagnosticContains(disabledRegistry, "without channelConfigs metadata");
const allowlistRegistry = loadPluginManifestRegistry({
config: { plugins: { allow: ["other-plugin"] } },
candidates: [candidate],
});
expect(allowlistRegistry.diagnostics.map((diagnostic) => diagnostic.message)).not.toEqual(
expect.arrayContaining([expect.stringContaining("without channelConfigs metadata")]),
);
expectNoRegistryDiagnosticContains(allowlistRegistry, "without channelConfigs metadata");
});
it("suppresses duplicate warnings for explicit installed globals overriding bundled plugins", () => {
@@ -590,12 +640,10 @@ describe("loadPluginManifestRegistry", () => {
});
expect(registry.plugins).toHaveLength(1);
expect(registry.plugins[0]).toEqual(
expect.objectContaining({
origin: "config",
trustedOfficialInstall: true,
}),
);
expectRecordFields(registry.plugins[0], "plugin", {
origin: "config",
trustedOfficialInstall: true,
});
});
it("does not trust unrecorded globals that spoof official ids", () => {
@@ -1064,16 +1112,12 @@ describe("loadPluginManifestRegistry", () => {
expect(registry.plugins[0]?.providerAuthEnvVars).toEqual({
openai: ["OPENAI_API_KEY"],
});
expect(registry.diagnostics).toContainEqual(
expect.objectContaining({
level: "warn",
pluginId: "external-openai",
source: path.join(dir, "openclaw.plugin.json"),
message: expect.stringContaining(
"providerAuthEnvVars is deprecated compatibility metadata",
),
}),
);
expectDiagnosticFields(registry, {
level: "warn",
pluginId: "external-openai",
source: path.join(dir, "openclaw.plugin.json"),
messageIncludes: "providerAuthEnvVars is deprecated compatibility metadata",
});
});
it("does not report deprecated providerAuthEnvVars when setup providers mirror env vars", () => {
@@ -1096,12 +1140,9 @@ describe("loadPluginManifestRegistry", () => {
origin: "global",
});
expect(registry.diagnostics).not.toContainEqual(
expect.objectContaining({
message: expect.stringContaining(
"providerAuthEnvVars is deprecated compatibility metadata",
),
}),
expectNoRegistryDiagnosticContains(
registry,
"providerAuthEnvVars is deprecated compatibility metadata",
);
});
@@ -1148,14 +1189,12 @@ describe("loadPluginManifestRegistry", () => {
});
expect(registry.plugins[0]?.channels).toEqual(["external-chat"]);
expect(registry.diagnostics).toContainEqual(
expect.objectContaining({
level: "warn",
pluginId: "external-chat",
source: path.join(dir, "openclaw.plugin.json"),
message: expect.stringContaining("without channelConfigs metadata"),
}),
);
expectDiagnosticFields(registry, {
level: "warn",
pluginId: "external-chat",
source: path.join(dir, "openclaw.plugin.json"),
messageIncludes: "without channelConfigs metadata",
});
});
it("sanitizes manifest-controlled fields in channel config descriptor diagnostics", () => {
@@ -1208,13 +1247,11 @@ describe("loadPluginManifestRegistry", () => {
origin: "global",
});
expect(registry.plugins[0]?.channelConfigs?.["external-chat"]?.schema).toMatchObject({
expectRecordFields(registry.plugins[0]?.channelConfigs?.["external-chat"]?.schema, "schema", {
type: "object",
additionalProperties: false,
});
expect(registry.diagnostics.map((diagnostic) => diagnostic.message)).not.toEqual(
expect.arrayContaining([expect.stringContaining("without channelConfigs metadata")]),
);
expectNoRegistryDiagnosticContains(registry, "without channelConfigs metadata");
});
it("hydrates supplemental official external catalog contracts for lagging npm manifests", () => {
@@ -1235,17 +1272,15 @@ describe("loadPluginManifestRegistry", () => {
]);
expect(registry.plugins[0]?.contracts?.tools).toEqual(["wecom_mcp"]);
expect(registry.plugins[0]?.channelConfigs?.wecom).toEqual(
expect.objectContaining({
const wecomConfig = expectRecordFields(
registry.plugins[0]?.channelConfigs?.wecom,
"wecom config",
{
label: "WeCom",
schema: expect.objectContaining({
type: "object",
}),
}),
);
expect(registry.diagnostics.map((diagnostic) => diagnostic.message)).not.toEqual(
expect.arrayContaining([expect.stringContaining("without channelConfigs metadata")]),
},
);
expectRecordFields(wecomConfig.schema, "wecom schema", { type: "object" });
expectNoRegistryDiagnosticContains(registry, "without channelConfigs metadata");
});
it("fills missing official external catalog descriptors for partial npm channel configs", () => {
@@ -1276,18 +1311,20 @@ describe("loadPluginManifestRegistry", () => {
}),
]);
expect(registry.plugins[0]?.channelConfigs?.wecom).toEqual(
expect.objectContaining({
const wecomConfig = expectRecordFields(
registry.plugins[0]?.channelConfigs?.wecom,
"wecom config",
{
label: "WeCom",
description: "Enterprise WeChat conversation channel.",
schema: expect.objectContaining({
additionalProperties: false,
properties: {
corpId: { type: "string" },
},
}),
}),
},
);
expectRecordFields(wecomConfig.schema, "wecom schema", {
additionalProperties: false,
properties: {
corpId: { type: "string" },
},
});
});
it("drops prototype-polluting channel config keys from plugin manifests", () => {
@@ -1338,7 +1375,7 @@ describe("loadPluginManifestRegistry", () => {
expect(Object.prototype.hasOwnProperty.call(channelConfigs, "__proto__")).toBe(false);
expect(Object.prototype.hasOwnProperty.call(channelConfigs, "constructor")).toBe(false);
expect(Object.prototype.hasOwnProperty.call(channelConfigs, "prototype")).toBe(false);
expect(channelConfigs["safe-chat"]?.schema).toMatchObject({
expectRecordFields(channelConfigs["safe-chat"]?.schema, "safe-chat schema", {
type: "object",
additionalProperties: false,
});
@@ -1754,13 +1791,11 @@ describe("loadPluginManifestRegistry", () => {
}),
]);
expect(registry.plugins[0]?.channelConfigs?.telegram).toEqual(
expect.objectContaining({
schema: expect.objectContaining({
type: "object",
}),
}),
const telegramConfig = requireRecord(
registry.plugins[0]?.channelConfigs?.telegram,
"telegram config",
);
expectRecordFields(telegramConfig.schema, "telegram schema", { type: "object" });
});
it("preserves manifest-owned config contracts from plugin manifests", () => {
@@ -1935,9 +1970,7 @@ describe("loadPluginManifestRegistry", () => {
});
expect(registry.plugins.map((plugin) => plugin.id)).toEqual(["codex"]);
expect(registry.diagnostics.map((diag) => diag.message)).not.toEqual(
expect.arrayContaining([expect.stringContaining("openclaw.install.minHostVersion must use")]),
);
expectNoRegistryDiagnosticContains(registry, "openclaw.install.minHostVersion must use");
});
it("does not runtime-gate bundled source plugins by install minHostVersion", () => {
@@ -1963,9 +1996,7 @@ describe("loadPluginManifestRegistry", () => {
});
expect(registry.plugins.map((plugin) => plugin.id)).toContain("codex");
expect(registry.diagnostics.map((diag) => diag.message)).not.toEqual(
expect.arrayContaining([expect.stringContaining("requires OpenClaw")]),
);
expectNoRegistryDiagnosticContains(registry, "requires OpenClaw");
});
it.each([
@@ -2115,8 +2146,8 @@ describe("loadPluginManifestRegistry", () => {
bundleFormat: "codex",
hooks: ["hooks"],
skills: ["skills"],
bundleCapabilities: expect.arrayContaining(["hooks", "skills"]),
},
expectedCapabilities: ["hooks", "skills"],
},
{
name: "loads Claude bundle manifests with command roots and settings files",
@@ -2143,8 +2174,8 @@ describe("loadPluginManifestRegistry", () => {
bundleFormat: "claude",
skills: ["skill-packs/starter", "commands-pack"],
settingsFiles: ["settings.json"],
bundleCapabilities: expect.arrayContaining(["skills", "commands", "settings"]),
},
expectedCapabilities: ["skills", "commands", "settings"],
},
{
name: "loads manifestless Claude bundles into the registry",
@@ -2164,8 +2195,8 @@ describe("loadPluginManifestRegistry", () => {
bundleFormat: "claude",
skills: ["commands"],
settingsFiles: ["settings.json"],
bundleCapabilities: expect.arrayContaining(["skills", "commands", "settings"]),
},
expectedCapabilities: ["skills", "commands", "settings"],
},
{
name: "loads Cursor bundle manifests into the registry",
@@ -2191,16 +2222,10 @@ describe("loadPluginManifestRegistry", () => {
format: "bundle",
bundleFormat: "cursor",
skills: ["skills", ".cursor/commands"],
bundleCapabilities: expect.arrayContaining([
"skills",
"commands",
"rules",
"hooks",
"mcpServers",
]),
},
expectedCapabilities: ["skills", "commands", "rules", "hooks", "mcpServers"],
},
] as const)("$name", ({ idHint, bundleFormat, setup, expected }) => {
] as const)("$name", ({ idHint, bundleFormat, setup, expected, expectedCapabilities }) => {
const registry = loadBundleRegistry({
idHint,
bundleFormat,
@@ -2208,7 +2233,12 @@ describe("loadPluginManifestRegistry", () => {
});
expect(registry.plugins).toHaveLength(1);
expect(registry.plugins[0]).toMatchObject(expected);
expectRecordFields(registry.plugins[0], "bundle plugin", expected);
expectArrayIncludesAll(
registry.plugins[0]?.bundleCapabilities,
expectedCapabilities,
"bundle capabilities",
);
});
it("prefers higher-precedence origins for the same physical directory (config > workspace > global > bundled)", () => {
@@ -2376,12 +2406,8 @@ describe("loadPluginManifestRegistry", () => {
});
expect(olderHost.plugins).toStrictEqual([]);
expect(olderHost.diagnostics.map((diag) => diag.message)).toEqual(
expect.arrayContaining([expect.stringContaining("this host is 2026.3.21")]),
);
expectRegistryDiagnosticContains(olderHost, "this host is 2026.3.21");
expect(newerHost.plugins.map((plugin) => plugin.id)).toContain("synology-chat");
expect(newerHost.diagnostics.map((diag) => diag.message)).not.toEqual(
expect.arrayContaining([expect.stringContaining("this host is 2026.3.21")]),
);
expectNoRegistryDiagnosticContains(newerHost, "this host is 2026.3.21");
});
});