|
6 | 6 | } from "openclaw/plugin-sdk/agent-runtime-test-contracts"; |
7 | 7 | import { beforeEach, describe, expect, it, vi } from "vitest"; |
8 | 8 | import { getPluginToolMeta, setPluginToolMeta } from "../../plugins/tools.js"; |
| 9 | +import type { RuntimeToolSchemaDiagnostic } from "../tool-schema-projection.js"; |
9 | 10 | import { logAgentRuntimeToolDiagnostics, normalizeAgentRuntimeTools } from "./tools.js"; |
10 | 11 | import type { AgentRuntimePlan } from "./types.js"; |
11 | 12 |
|
@@ -55,6 +56,82 @@ describe("AgentRuntimePlan tool policy helpers", () => { |
55 | 56 | }); |
56 | 57 | }); |
57 | 58 |
|
| 59 | + it("quarantines unreadable tools before RuntimePlan normalization", () => { |
| 60 | + const healthy = { ...createParameterFreeTool(), name: "healthy" } as AgentTool; |
| 61 | + const unreadable = { ...createParameterFreeTool(), name: "fuzzplugin_unreadable" } as AgentTool; |
| 62 | + Object.defineProperty(unreadable, "parameters", { |
| 63 | + enumerable: true, |
| 64 | + get() { |
| 65 | + throw new Error("fuzzplugin parameters getter exploded"); |
| 66 | + }, |
| 67 | + }); |
| 68 | + const tools = [unreadable, healthy]; |
| 69 | + const diagnostics: RuntimeToolSchemaDiagnostic[][] = []; |
| 70 | + const normalize = vi.fn((entries: AgentTool[]) => entries); |
| 71 | + const runtimePlan = { |
| 72 | + tools: { |
| 73 | + normalize, |
| 74 | + logDiagnostics: vi.fn(), |
| 75 | + }, |
| 76 | + } as unknown as AgentRuntimePlan; |
| 77 | + |
| 78 | + expect( |
| 79 | + normalizeAgentRuntimeTools({ |
| 80 | + runtimePlan, |
| 81 | + tools, |
| 82 | + provider: "openai", |
| 83 | + onPreNormalizationSchemaDiagnostics: (entries) => diagnostics.push([...entries]), |
| 84 | + }), |
| 85 | + ).toEqual([healthy]); |
| 86 | + expect(normalize).toHaveBeenCalledWith([healthy], { |
| 87 | + workspaceDir: undefined, |
| 88 | + modelApi: undefined, |
| 89 | + model: undefined, |
| 90 | + }); |
| 91 | + expect(diagnostics).toEqual([ |
| 92 | + [ |
| 93 | + { |
| 94 | + toolName: "fuzzplugin_unreadable", |
| 95 | + toolIndex: 0, |
| 96 | + violations: ["fuzzplugin_unreadable.parameters is unreadable"], |
| 97 | + }, |
| 98 | + ], |
| 99 | + ]); |
| 100 | + }); |
| 101 | + |
| 102 | + it("quarantines non-object schemas before provider schema normalization", () => { |
| 103 | + const healthy = { ...createParameterFreeTool(), name: "healthy" } as AgentTool; |
| 104 | + const arraySchema = { |
| 105 | + ...createParameterFreeTool("fuzzplugin_array_root"), |
| 106 | + parameters: { type: "array", items: { type: "number" } }, |
| 107 | + } as unknown as AgentTool; |
| 108 | + const diagnostics: RuntimeToolSchemaDiagnostic[][] = []; |
| 109 | + mocks.normalizeProviderToolSchemas.mockImplementationOnce(({ tools: entries }) => entries); |
| 110 | + |
| 111 | + expect( |
| 112 | + normalizeAgentRuntimeTools({ |
| 113 | + tools: [arraySchema, healthy], |
| 114 | + provider: "openai", |
| 115 | + onPreNormalizationSchemaDiagnostics: (entries) => diagnostics.push([...entries]), |
| 116 | + }), |
| 117 | + ).toEqual([healthy]); |
| 118 | + expect(mocks.normalizeProviderToolSchemas).toHaveBeenCalledWith( |
| 119 | + expect.objectContaining({ |
| 120 | + tools: [healthy], |
| 121 | + provider: "openai", |
| 122 | + }), |
| 123 | + ); |
| 124 | + expect(diagnostics).toEqual([ |
| 125 | + [ |
| 126 | + { |
| 127 | + toolName: "fuzzplugin_array_root", |
| 128 | + toolIndex: 0, |
| 129 | + violations: ['fuzzplugin_array_root.parameters.type must be "object"'], |
| 130 | + }, |
| 131 | + ], |
| 132 | + ]); |
| 133 | + }); |
| 134 | + |
58 | 135 | it("accepts legacy optional model fields while normalizing RuntimePlan context", () => { |
59 | 136 | const tools = [createParameterFreeTool()] as AgentTool[]; |
60 | 137 | const normalize = vi.fn(() => tools); |
@@ -145,6 +222,96 @@ describe("AgentRuntimePlan tool policy helpers", () => { |
145 | 222 | }); |
146 | 223 | }); |
147 | 224 |
|
| 225 | + it("does not reread quarantined tools while preserving normalized metadata", () => { |
| 226 | + const unreadableName = { |
| 227 | + ...createParameterFreeTool("fuzzplugin_unreadable_name"), |
| 228 | + } as AgentTool; |
| 229 | + Object.defineProperty(unreadableName, "name", { |
| 230 | + enumerable: true, |
| 231 | + get() { |
| 232 | + throw new Error("fuzzplugin name getter exploded"); |
| 233 | + }, |
| 234 | + }); |
| 235 | + const healthy = createParameterFreeTool("fixture__lookup_note") as AgentTool; |
| 236 | + setPluginToolMeta(healthy, { |
| 237 | + pluginId: "bundle-mcp", |
| 238 | + optional: false, |
| 239 | + mcp: { |
| 240 | + serverName: "fixture", |
| 241 | + safeServerName: "fixture", |
| 242 | + toolName: "lookup_note", |
| 243 | + operation: "tool", |
| 244 | + }, |
| 245 | + }); |
| 246 | + const normalized = { |
| 247 | + ...healthy, |
| 248 | + parameters: normalizedParameterFreeSchema(), |
| 249 | + }; |
| 250 | + const diagnostics: RuntimeToolSchemaDiagnostic[][] = []; |
| 251 | + mocks.normalizeProviderToolSchemas.mockReturnValueOnce([normalized]); |
| 252 | + |
| 253 | + const result = normalizeAgentRuntimeTools({ |
| 254 | + tools: [unreadableName, healthy], |
| 255 | + provider: "openai", |
| 256 | + onPreNormalizationSchemaDiagnostics: (entries) => diagnostics.push([...entries]), |
| 257 | + }); |
| 258 | + |
| 259 | + expect(result).toEqual([normalized]); |
| 260 | + expect(getPluginToolMeta(result[0])).toMatchObject({ |
| 261 | + pluginId: "bundle-mcp", |
| 262 | + mcp: { |
| 263 | + serverName: "fixture", |
| 264 | + toolName: "lookup_note", |
| 265 | + }, |
| 266 | + }); |
| 267 | + expect(diagnostics).toEqual([ |
| 268 | + [ |
| 269 | + { |
| 270 | + toolName: "tool[0]", |
| 271 | + toolIndex: 0, |
| 272 | + violations: ["tool[0].name is unreadable"], |
| 273 | + }, |
| 274 | + ], |
| 275 | + ]); |
| 276 | + }); |
| 277 | + |
| 278 | + it("quarantines unreadable tools before provider schema normalization", () => { |
| 279 | + const healthy = { ...createParameterFreeTool(), name: "healthy" } as AgentTool; |
| 280 | + const unreadable = { ...createParameterFreeTool(), name: "fuzzplugin_unreadable" } as AgentTool; |
| 281 | + Object.defineProperty(unreadable, "parameters", { |
| 282 | + enumerable: true, |
| 283 | + get() { |
| 284 | + throw new Error("fuzzplugin parameters getter exploded"); |
| 285 | + }, |
| 286 | + }); |
| 287 | + const tools = [unreadable, healthy]; |
| 288 | + const diagnostics: RuntimeToolSchemaDiagnostic[][] = []; |
| 289 | + mocks.normalizeProviderToolSchemas.mockImplementationOnce(({ tools: entries }) => entries); |
| 290 | + |
| 291 | + expect( |
| 292 | + normalizeAgentRuntimeTools({ |
| 293 | + tools, |
| 294 | + provider: "openai", |
| 295 | + onPreNormalizationSchemaDiagnostics: (entries) => diagnostics.push([...entries]), |
| 296 | + }), |
| 297 | + ).toEqual([healthy]); |
| 298 | + expect(mocks.normalizeProviderToolSchemas).toHaveBeenCalledWith( |
| 299 | + expect.objectContaining({ |
| 300 | + tools: [healthy], |
| 301 | + provider: "openai", |
| 302 | + }), |
| 303 | + ); |
| 304 | + expect(diagnostics).toEqual([ |
| 305 | + [ |
| 306 | + { |
| 307 | + toolName: "fuzzplugin_unreadable", |
| 308 | + toolIndex: 0, |
| 309 | + violations: ["fuzzplugin_unreadable.parameters is unreadable"], |
| 310 | + }, |
| 311 | + ], |
| 312 | + ]); |
| 313 | + }); |
| 314 | + |
148 | 315 | it("can normalize without cold-loading provider runtime plugins", () => { |
149 | 316 | const tools = [createParameterFreeTool()] as AgentTool[]; |
150 | 317 |
|
|
0 commit comments