mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-08 19:42:56 +00:00
fix(doctor): report runtime tool schema errors
This commit is contained in:
committed by
Peter Steinberger
parent
3171278372
commit
dc05f598bb
168
src/flows/doctor-core-checks.runtime-errors.test.ts
Normal file
168
src/flows/doctor-core-checks.runtime-errors.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AnyAgentTool } from "../agents/tools/common.js";
|
||||
import { setPluginToolMeta } from "../plugins/tools.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
createBundleMcpToolRuntime: vi.fn(),
|
||||
createOpenClawCodingTools: vi.fn(),
|
||||
disposeBundleRuntime: vi.fn(),
|
||||
loadModelCatalog: vi.fn(async (): Promise<Array<Record<string, unknown>>> => []),
|
||||
normalizeProviderToolSchemasWithPlugin: vi.fn(),
|
||||
resolveDefaultModelForAgent: vi.fn(() => ({ provider: "openai", model: "gpt-5.5" })),
|
||||
}));
|
||||
|
||||
vi.mock("../agents/model-catalog.js", () => ({
|
||||
findModelInCatalog: (
|
||||
catalog: Array<{ provider?: string; id?: string }>,
|
||||
provider: string,
|
||||
modelId: string,
|
||||
) => catalog.find((entry) => entry.provider === provider && entry.id === modelId),
|
||||
loadModelCatalog: mocks.loadModelCatalog,
|
||||
}));
|
||||
|
||||
vi.mock("../agents/model-selection.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../agents/model-selection.js")>()),
|
||||
resolveDefaultModelForAgent: mocks.resolveDefaultModelForAgent,
|
||||
}));
|
||||
|
||||
vi.mock("../agents/agent-bundle-mcp-tools.js", () => ({
|
||||
createBundleMcpToolRuntime: mocks.createBundleMcpToolRuntime,
|
||||
}));
|
||||
|
||||
vi.mock("../agents/agent-tools.js", () => ({
|
||||
createOpenClawCodingTools: mocks.createOpenClawCodingTools,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/provider-runtime.js", () => ({
|
||||
inspectProviderToolSchemasWithPlugin: () => [],
|
||||
normalizeProviderToolSchemasWithPlugin: mocks.normalizeProviderToolSchemasWithPlugin,
|
||||
}));
|
||||
|
||||
const { collectRuntimeToolSchemaFindings } = await import("./doctor-core-checks.runtime.js");
|
||||
|
||||
function tool(name: string, parameters: unknown): AnyAgentTool {
|
||||
return {
|
||||
name,
|
||||
label: name,
|
||||
description: name,
|
||||
parameters,
|
||||
execute: async () => ({ text: "ok" }),
|
||||
} as unknown as AnyAgentTool;
|
||||
}
|
||||
|
||||
function bundleMcpTool(name: string, parameters: unknown): AnyAgentTool {
|
||||
const entry = tool(name, parameters);
|
||||
setPluginToolMeta(entry, { pluginId: "bundle-mcp", optional: false });
|
||||
return entry;
|
||||
}
|
||||
|
||||
describe("doctor runtime tool schema error handling", () => {
|
||||
beforeEach(() => {
|
||||
mocks.createOpenClawCodingTools.mockReset().mockReturnValue([]);
|
||||
mocks.createBundleMcpToolRuntime.mockReset().mockResolvedValue({
|
||||
tools: [],
|
||||
dispose: mocks.disposeBundleRuntime,
|
||||
});
|
||||
mocks.disposeBundleRuntime.mockReset().mockResolvedValue(undefined);
|
||||
mocks.loadModelCatalog.mockClear();
|
||||
mocks.normalizeProviderToolSchemasWithPlugin
|
||||
.mockReset()
|
||||
.mockImplementation(({ context }) => context.tools);
|
||||
mocks.resolveDefaultModelForAgent.mockClear();
|
||||
});
|
||||
|
||||
it("reports agent runtime tool construction failures without aborting schema checks", async () => {
|
||||
mocks.createOpenClawCodingTools.mockImplementationOnce(() => {
|
||||
throw new Error("fuzzplugin startup failed");
|
||||
});
|
||||
|
||||
await expect(collectRuntimeToolSchemaFindings({})).resolves.toContainEqual({
|
||||
checkId: "core/doctor/runtime-tool-schemas",
|
||||
severity: "error",
|
||||
message: "Agent main runtime tool schema validation could not load the runtime tool set.",
|
||||
path: "agents.main.tools",
|
||||
requirement: "fuzzplugin startup failed",
|
||||
fixHint:
|
||||
"Fix provider/plugin tool loading errors, then rerun doctor before relying on assistant tool startup.",
|
||||
});
|
||||
expect(mocks.createBundleMcpToolRuntime).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.disposeBundleRuntime).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reports agent runtime tool normalization failures without aborting doctor", async () => {
|
||||
mocks.createOpenClawCodingTools.mockReturnValueOnce([
|
||||
tool("fuzzplugin_move_angles", { type: "object", properties: {} }),
|
||||
]);
|
||||
mocks.normalizeProviderToolSchemasWithPlugin.mockImplementation(({ context }) => {
|
||||
const tools = context.tools as AnyAgentTool[];
|
||||
if (tools.some((entry) => entry.name === "fuzzplugin_move_angles")) {
|
||||
throw new Error("fuzzplugin schema normalization failed");
|
||||
}
|
||||
return tools;
|
||||
});
|
||||
|
||||
await expect(collectRuntimeToolSchemaFindings({})).resolves.toContainEqual({
|
||||
checkId: "core/doctor/runtime-tool-schemas",
|
||||
severity: "error",
|
||||
message:
|
||||
"Agent main runtime tool schema validation could not normalize the runtime tool set.",
|
||||
path: "agents.main.tools",
|
||||
requirement: "fuzzplugin schema normalization failed",
|
||||
fixHint:
|
||||
"Fix provider/plugin schema normalization errors, then rerun doctor before relying on assistant tool startup.",
|
||||
});
|
||||
expect(mocks.disposeBundleRuntime).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reports unreadable agent runtime tool schemas without aborting doctor", async () => {
|
||||
const unreadable = tool("fuzzplugin_unreadable", { type: "object", properties: {} });
|
||||
Object.defineProperty(unreadable, "parameters", {
|
||||
enumerable: true,
|
||||
get() {
|
||||
throw new Error("fuzzplugin parameters getter exploded");
|
||||
},
|
||||
});
|
||||
mocks.createOpenClawCodingTools.mockReturnValueOnce([
|
||||
unreadable,
|
||||
tool("healthy", { type: "object", properties: {} }),
|
||||
]);
|
||||
|
||||
await expect(collectRuntimeToolSchemaFindings({})).resolves.toContainEqual({
|
||||
checkId: "core/doctor/runtime-tool-schemas",
|
||||
severity: "error",
|
||||
message:
|
||||
"Agent main tool fuzzplugin_unreadable has an unsupported input schema for runtime projection.",
|
||||
path: "tools.fuzzplugin_unreadable",
|
||||
target: "fuzzplugin_unreadable",
|
||||
requirement: "fuzzplugin_unreadable.parameters is unreadable",
|
||||
fixHint:
|
||||
"Disable or update the offending plugin/tool so its parameters are a JSON object schema, then rerun doctor.",
|
||||
});
|
||||
expect(mocks.disposeBundleRuntime).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reports bundle MCP runtime tool normalization failures without aborting doctor", async () => {
|
||||
mocks.createBundleMcpToolRuntime.mockResolvedValueOnce({
|
||||
tools: [bundleMcpTool("fuzzplugin__move_angles", { type: "object", properties: {} })],
|
||||
dispose: mocks.disposeBundleRuntime,
|
||||
});
|
||||
mocks.normalizeProviderToolSchemasWithPlugin.mockImplementation(({ context }) => {
|
||||
const tools = context.tools as AnyAgentTool[];
|
||||
if (tools.some((entry) => entry.name === "fuzzplugin__move_angles")) {
|
||||
throw new Error("fuzzplugin MCP schema normalization failed");
|
||||
}
|
||||
return tools;
|
||||
});
|
||||
|
||||
await expect(collectRuntimeToolSchemaFindings({})).resolves.toContainEqual({
|
||||
checkId: "core/doctor/runtime-tool-schemas",
|
||||
severity: "error",
|
||||
message: "Configured MCP tool schema validation could not normalize the runtime tool set.",
|
||||
path: "mcp.servers",
|
||||
requirement: "fuzzplugin MCP schema normalization failed",
|
||||
fixHint:
|
||||
"Fix provider/plugin schema normalization errors, then rerun doctor before relying on assistant tool startup.",
|
||||
});
|
||||
expect(mocks.disposeBundleRuntime).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -606,27 +606,37 @@ function collectBundleMcpRuntimeToolSchemaFindings(params: {
|
||||
modelId: params.modelRef.model,
|
||||
warn: () => {},
|
||||
});
|
||||
const preNormalizationDiagnostics: RuntimeToolSchemaDiagnostic[] = [];
|
||||
const normalizedTools = normalizeAgentRuntimeTools({
|
||||
tools: activeBundleTools,
|
||||
provider: params.modelRef.provider,
|
||||
config: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: process.env,
|
||||
modelId: params.modelRef.model,
|
||||
modelApi: params.model.api,
|
||||
model: params.model,
|
||||
onPreNormalizationSchemaDiagnostics: (diagnostics) =>
|
||||
preNormalizationDiagnostics.push(...diagnostics),
|
||||
});
|
||||
const preNormalizationFindings: HealthFinding[] = [];
|
||||
|
||||
let normalizedTools: AnyAgentTool[];
|
||||
try {
|
||||
normalizedTools = normalizeAgentRuntimeTools({
|
||||
tools: activeBundleTools,
|
||||
provider: params.modelRef.provider,
|
||||
config: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: process.env,
|
||||
modelId: params.modelRef.model,
|
||||
modelApi: params.model.api,
|
||||
model: params.model,
|
||||
onPreNormalizationSchemaDiagnostics: (diagnostics, sourceTools) => {
|
||||
preNormalizationFindings.push(
|
||||
...diagnostics.map((diagnostic) =>
|
||||
toolSchemaDiagnosticToFinding({
|
||||
agentId: params.agentId,
|
||||
tools: sourceTools,
|
||||
diagnostic,
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return [...preNormalizationFindings, bundleMcpRuntimeNormalizationFailureFinding(error)];
|
||||
}
|
||||
|
||||
return [
|
||||
...preNormalizationDiagnostics.map((diagnostic) =>
|
||||
toolSchemaDiagnosticToFinding({
|
||||
agentId: params.agentId,
|
||||
tools: activeBundleTools,
|
||||
diagnostic,
|
||||
}),
|
||||
),
|
||||
...preNormalizationFindings,
|
||||
...collectToolSchemaFindings({
|
||||
agentId: params.agentId,
|
||||
tools: normalizedTools,
|
||||
@@ -634,6 +644,114 @@ function collectBundleMcpRuntimeToolSchemaFindings(params: {
|
||||
];
|
||||
}
|
||||
|
||||
function agentRuntimeToolLoadFailureFinding(params: {
|
||||
agentId: string;
|
||||
error: unknown;
|
||||
}): HealthFinding {
|
||||
return {
|
||||
checkId: "core/doctor/runtime-tool-schemas",
|
||||
severity: "error",
|
||||
message: `Agent ${params.agentId} runtime tool schema validation could not load the runtime tool set.`,
|
||||
path: `agents.${params.agentId}.tools`,
|
||||
requirement: formatErrorMessage(params.error),
|
||||
fixHint:
|
||||
"Fix provider/plugin tool loading errors, then rerun doctor before relying on assistant tool startup.",
|
||||
};
|
||||
}
|
||||
|
||||
function agentRuntimeToolNormalizationFailureFinding(params: {
|
||||
agentId: string;
|
||||
error: unknown;
|
||||
}): HealthFinding {
|
||||
return {
|
||||
checkId: "core/doctor/runtime-tool-schemas",
|
||||
severity: "error",
|
||||
message: `Agent ${params.agentId} runtime tool schema validation could not normalize the runtime tool set.`,
|
||||
path: `agents.${params.agentId}.tools`,
|
||||
requirement: formatErrorMessage(params.error),
|
||||
fixHint:
|
||||
"Fix provider/plugin schema normalization errors, then rerun doctor before relying on assistant tool startup.",
|
||||
};
|
||||
}
|
||||
|
||||
function collectAgentRuntimeToolSchemaFindings(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
workspaceDir: string;
|
||||
modelRef: { provider: string; model: string };
|
||||
model: ProviderRuntimeModel;
|
||||
}): readonly HealthFinding[] {
|
||||
let tools: AnyAgentTool[];
|
||||
try {
|
||||
tools = createOpenClawCodingTools({
|
||||
agentId: params.agentId,
|
||||
workspaceDir: params.workspaceDir,
|
||||
config: params.cfg,
|
||||
modelProvider: params.modelRef.provider,
|
||||
modelId: params.modelRef.model,
|
||||
modelApi: params.model.api,
|
||||
modelCompat: params.model.compat,
|
||||
modelContextWindowTokens: params.model.contextWindow,
|
||||
allowGatewaySubagentBinding: true,
|
||||
emitBeforeToolCallDiagnostics: false,
|
||||
});
|
||||
} catch (error) {
|
||||
return [agentRuntimeToolLoadFailureFinding({ agentId: params.agentId, error })];
|
||||
}
|
||||
|
||||
const preNormalizationFindings: HealthFinding[] = [];
|
||||
|
||||
let normalizedTools: AnyAgentTool[];
|
||||
try {
|
||||
normalizedTools = normalizeAgentRuntimeTools({
|
||||
tools,
|
||||
provider: params.modelRef.provider,
|
||||
config: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: process.env,
|
||||
modelId: params.modelRef.model,
|
||||
modelApi: params.model.api,
|
||||
model: params.model,
|
||||
onPreNormalizationSchemaDiagnostics: (diagnostics, sourceTools) => {
|
||||
preNormalizationFindings.push(
|
||||
...diagnostics.map((diagnostic) =>
|
||||
toolSchemaDiagnosticToFinding({
|
||||
agentId: params.agentId,
|
||||
tools: sourceTools,
|
||||
diagnostic,
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return [
|
||||
...preNormalizationFindings,
|
||||
agentRuntimeToolNormalizationFailureFinding({ agentId: params.agentId, error }),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
...preNormalizationFindings,
|
||||
...collectToolSchemaFindings({
|
||||
agentId: params.agentId,
|
||||
tools: normalizedTools,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function bundleMcpRuntimeNormalizationFailureFinding(error: unknown): HealthFinding {
|
||||
return {
|
||||
checkId: "core/doctor/runtime-tool-schemas",
|
||||
severity: "error",
|
||||
message: "Configured MCP tool schema validation could not normalize the runtime tool set.",
|
||||
path: "mcp.servers",
|
||||
requirement: formatErrorMessage(error),
|
||||
fixHint:
|
||||
"Fix provider/plugin schema normalization errors, then rerun doctor before relying on assistant tool startup.",
|
||||
};
|
||||
}
|
||||
|
||||
function bundleMcpRuntimeLoadFailureFinding(error: unknown): HealthFinding {
|
||||
return {
|
||||
checkId: "core/doctor/runtime-tool-schemas",
|
||||
@@ -802,42 +920,13 @@ export async function collectRuntimeToolSchemaFindings(
|
||||
if (!supportsModelTools(model)) {
|
||||
continue;
|
||||
}
|
||||
const tools = createOpenClawCodingTools({
|
||||
agentId,
|
||||
workspaceDir,
|
||||
config: cfg,
|
||||
modelProvider: modelRef.provider,
|
||||
modelId: modelRef.model,
|
||||
modelApi: model.api,
|
||||
modelCompat: model.compat,
|
||||
modelContextWindowTokens: model.contextWindow,
|
||||
allowGatewaySubagentBinding: true,
|
||||
emitBeforeToolCallDiagnostics: false,
|
||||
});
|
||||
const preNormalizationDiagnostics: RuntimeToolSchemaDiagnostic[] = [];
|
||||
const normalizedTools = normalizeAgentRuntimeTools({
|
||||
tools,
|
||||
provider: modelRef.provider,
|
||||
config: cfg,
|
||||
workspaceDir,
|
||||
env: process.env,
|
||||
modelId: modelRef.model,
|
||||
modelApi: model.api,
|
||||
model,
|
||||
onPreNormalizationSchemaDiagnostics: (diagnostics) =>
|
||||
preNormalizationDiagnostics.push(...diagnostics),
|
||||
});
|
||||
findings.push(
|
||||
...preNormalizationDiagnostics.map((diagnostic) =>
|
||||
toolSchemaDiagnosticToFinding({
|
||||
agentId,
|
||||
tools,
|
||||
diagnostic,
|
||||
}),
|
||||
),
|
||||
...collectToolSchemaFindings({
|
||||
...collectAgentRuntimeToolSchemaFindings({
|
||||
cfg,
|
||||
agentId,
|
||||
tools: normalizedTools,
|
||||
workspaceDir,
|
||||
modelRef,
|
||||
model,
|
||||
}),
|
||||
);
|
||||
if (!shouldCreateBundleMcpRuntimeForAttempt({ toolsEnabled: true })) {
|
||||
|
||||
Reference in New Issue
Block a user