fix(plugins): snapshot tool metadata

This commit is contained in:
Vincent Koc
2026-06-05 14:18:55 +02:00
parent 4c7e815dc6
commit 5366dbda13
2 changed files with 154 additions and 6 deletions

View File

@@ -0,0 +1,104 @@
// Tool metadata registration tests cover plugin-owned metadata snapshotting.
import {
createPluginRegistryFixture,
registerTestPlugin,
} from "openclaw/plugin-sdk/plugin-test-contracts";
import { afterEach, describe, expect, it } from "vitest";
import { buildEffectiveToolInventoryEntries } from "../../agents/tools-effective-inventory-build.js";
import type { AnyAgentTool } from "../../agents/tools/common.js";
import type { PluginToolMetadataRegistration } from "../host-hooks.js";
import { createEmptyPluginRegistry } from "../registry-empty.js";
import { setActivePluginRegistry } from "../runtime.js";
import { createPluginRecord } from "../status.test-helpers.js";
import { setPluginToolMeta } from "../tools.js";
describe("plugin tool metadata registration", () => {
afterEach(() => {
setActivePluginRegistry(createEmptyPluginRegistry());
});
it("snapshots metadata before effective tool inventory projection", () => {
let toolNameReads = 0;
let displayNameReads = 0;
let descriptionReads = 0;
let riskReads = 0;
let tagsReads = 0;
const { config, registry } = createPluginRegistryFixture();
registerTestPlugin({
registry,
config,
record: createPluginRecord({
id: "volatile-metadata",
name: "Volatile Metadata",
contracts: { tools: ["volatile_tool"] },
}),
register(api) {
api.registerToolMetadata({
get toolName() {
toolNameReads += 1;
if (toolNameReads > 1) {
throw new Error("toolName getter re-read");
}
return "volatile_tool";
},
get displayName() {
displayNameReads += 1;
if (displayNameReads > 1) {
throw new Error("displayName getter re-read");
}
return "Volatile Tool";
},
get description() {
descriptionReads += 1;
if (descriptionReads > 1) {
throw new Error("description getter re-read");
}
return "Stable metadata description.";
},
get risk() {
riskReads += 1;
if (riskReads > 1) {
throw new Error("risk getter re-read");
}
return "medium";
},
get tags() {
tagsReads += 1;
if (tagsReads > 1) {
throw new Error("tags getter re-read");
}
return ["metadata", "fixture"];
},
} as PluginToolMetadataRegistration);
},
});
setActivePluginRegistry(registry.registry);
const tool = {
name: "volatile_tool",
label: "Raw label",
description: "Raw description",
parameters: { type: "object", properties: {} },
execute: async () => ({ text: "ok" }),
} as unknown as AnyAgentTool;
setPluginToolMeta(tool, { pluginId: "volatile-metadata", optional: false });
expect(buildEffectiveToolInventoryEntries([tool])).toEqual([
{
id: "volatile_tool",
label: "Volatile Tool",
description: "Stable metadata description.",
rawDescription: "Stable metadata description.",
source: "plugin",
pluginId: "volatile-metadata",
risk: "medium",
tags: ["metadata", "fixture"],
},
]);
expect(toolNameReads).toBe(1);
expect(displayNameReads).toBe(1);
expect(descriptionReads).toBe(1);
expect(riskReads).toBe(1);
expect(tagsReads).toBe(1);
});
});

View File

@@ -2274,8 +2274,48 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
});
};
const readToolMetadataFields = (
record: PluginRecord,
metadata: PluginToolMetadataRegistration,
):
| {
toolName: unknown;
displayName: unknown;
description: unknown;
risk: unknown;
tags: unknown;
}
| undefined => {
let toolName: unknown;
try {
toolName = metadata.toolName;
return {
toolName,
displayName: metadata.displayName,
description: metadata.description,
risk: metadata.risk,
tags: metadata.tags,
};
} catch (error) {
const normalizedToolName = normalizeOptionalHostHookString(toolName);
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message:
`tool metadata registration has unreadable fields` +
`${normalizedToolName ? `: ${normalizedToolName}` : ""}: ${formatErrorMessage(error)}`,
});
return undefined;
}
};
const registerToolMetadata = (record: PluginRecord, metadata: PluginToolMetadataRegistration) => {
const toolName = normalizeHostHookString(metadata.toolName);
const fields = readToolMetadataFields(record, metadata);
if (!fields) {
return;
}
const toolName = normalizeHostHookString(fields.toolName);
if (!toolName) {
pushDiagnostic({
level: "error",
@@ -2317,14 +2357,16 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
});
return;
}
const displayName = normalizeOptionalHostHookString(metadata.displayName);
const description = normalizeOptionalHostHookString(metadata.description);
const tags = normalizeHostHookStringList(metadata.tags);
const displayName = normalizeOptionalHostHookString(fields.displayName);
const description = normalizeOptionalHostHookString(fields.description);
const tags = normalizeHostHookStringList(fields.tags);
const risk = fields.risk;
if (
displayName === "" ||
description === "" ||
tags === null ||
(metadata.risk !== undefined && !["low", "medium", "high"].includes(metadata.risk))
(risk !== undefined &&
(typeof risk !== "string" || !["low", "medium", "high"].includes(risk)))
) {
pushDiagnostic({
level: "error",
@@ -2338,10 +2380,12 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
pluginId: record.id,
pluginName: record.name,
metadata: {
...metadata,
toolName,
...(displayName !== undefined ? { displayName } : {}),
...(description !== undefined ? { description } : {}),
...(risk !== undefined
? { risk: risk as NonNullable<PluginToolMetadataRegistration["risk"]> }
: {}),
...(tags !== undefined ? { tags } : {}),
},
source: record.source,