File size: 7,149 Bytes
76289e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
// Doctor runtime error tests cover error handling in core runtime checks.
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { AnyAgentTool } from "../agents/tools/common.js";
import { setPluginToolMeta } from "../plugins/tool-metadata.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),
}));

vi.mock("../agents/prepared-model-catalog.js", () => ({
  loadProviderScopedThinkingCatalog: vi.fn(async () => []),
  readPreparedModelCatalog: 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);
  });
});