From 447918a3ee0c2992ba9959023c5f4ae4d705c110 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Fri, 16 May 2025 20:56:25 +0530 Subject: [PATCH 01/57] feat: Add basic client and tool --- packages/toolbox-core/package-lock.json | 278 +++++++++++++++++- packages/toolbox-core/package.json | 5 + .../toolbox-core/src/toolbox_core/client.ts | 48 ++- .../toolbox-core/src/toolbox_core/protocol.ts | 130 ++++++++ .../toolbox-core/src/toolbox_core/tool.ts | 45 ++- .../toolbox-core/src/toolbox_core/utils.ts | 0 packages/toolbox-core/test/test.client.ts | 21 ++ packages/toolbox-core/test/test.e2e.ts | 12 + packages/toolbox-core/test/test_client.ts | 10 - 9 files changed, 521 insertions(+), 28 deletions(-) create mode 100644 packages/toolbox-core/src/toolbox_core/protocol.ts create mode 100644 packages/toolbox-core/src/toolbox_core/utils.ts create mode 100644 packages/toolbox-core/test/test.client.ts create mode 100644 packages/toolbox-core/test/test.e2e.ts delete mode 100644 packages/toolbox-core/test/test_client.ts diff --git a/packages/toolbox-core/package-lock.json b/packages/toolbox-core/package-lock.json index 6ae6bfa..4052e2f 100644 --- a/packages/toolbox-core/package-lock.json +++ b/packages/toolbox-core/package-lock.json @@ -8,6 +8,11 @@ "name": "@toolbox/core", "version": "0.1.0", "license": "Apache-2.0", + "dependencies": { + "axios": "^1.9.0", + "axois": "^0.0.1-security", + "zod": "^3.24.4" + }, "devDependencies": { "@types/jest": "^29.5.14", "cross-env": "^7.0.3", @@ -1698,6 +1703,28 @@ "dev": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axois": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/axois/-/axois-0.0.1-security.tgz", + "integrity": "sha512-8Nui4fwwyxHfjAfpDlg3Jt66EJA4i1D1eJch3D+wM/Oe+qhpyp7yfiszko/O5/adYu20wc37RG9/Eg8QIJHcvA==" + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -1928,6 +1955,19 @@ "semver": "^7.0.0" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2120,6 +2160,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2277,6 +2329,15 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2323,6 +2384,20 @@ "node": ">=6.0.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -2376,6 +2451,51 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3046,6 +3166,41 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3072,7 +3227,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3098,6 +3252,30 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -3108,6 +3286,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -3177,6 +3368,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3244,11 +3447,37 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4448,6 +4677,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/meow": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", @@ -4519,6 +4757,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -5013,6 +5272,12 @@ "node": ">= 6" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6135,6 +6400,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/packages/toolbox-core/package.json b/packages/toolbox-core/package.json index 06866e7..0de5ca8 100644 --- a/packages/toolbox-core/package.json +++ b/packages/toolbox-core/package.json @@ -43,5 +43,10 @@ "jest": "^29.7.0", "ts-jest": "^29.3.2", "typescript": "^5.8.3" + }, + "dependencies": { + "axios": "^1.9.0", + "axois": "^0.0.1-security", + "zod": "^3.24.4" } } diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index 63d5845..ab9c2e4 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -13,26 +13,58 @@ // limitations under the License. import {ToolboxTool} from './tool'; +import axios, { AxiosInstance, AxiosResponse } from 'axios'; +import {ZodManifestSchema, createZodObjectSchemaFromParameters} from './protocol'; class ToolboxClient { /** @private */ _baseUrl; + /** @private */ _session; /** * @param {string} url - The base URL for the Toolbox service API. */ - constructor(url: string) { + constructor(url: string, session?: AxiosInstance) { this._baseUrl = url; + this._session = session || axios.create({ baseURL: url }); } /** - * @param {int} num1 - First number. - * @param {int} num2 - Second number. - * @returns {int} - Mock API response. + * @param {string} toolName - Name of the tool. + * @returns {ToolboxTool} - A ToolboxTool instance. */ - async getToolResponse(num1: number, num2: number) { - const tool = ToolboxTool('tool1'); - const response = await tool({a: num1, b: num2}); - return response; + async loadTool(toolName: string): Promise> { + const url = `${this._baseUrl}/api/tool/${toolName}` + try { + const response: AxiosResponse = await this._session.get(url); + const responseData = response.data; + + console.log("DEBUGggggg, response data", responseData) + + const manifestResponse = ZodManifestSchema.safeParse(responseData); + if (manifestResponse.success) { + const manifest = manifestResponse.data; + if (manifest.tools && manifest.tools.hasOwnProperty(toolName)) { + const specificToolSchema = manifest.tools[toolName]; + const paramZodSchema = createZodObjectSchemaFromParameters(specificToolSchema.parameters) + + return ToolboxTool( + this._session, + this._baseUrl, + toolName, + specificToolSchema.description, + paramZodSchema + ); + } else { + throw new Error(`Tool "${toolName}" not found in manifest.`); + } + } else { + throw new Error(`Invalid manifest structure received: ${manifestResponse.error.message}`); + } + + } catch (error) { + console.error(`Error fetching data from ${url}:`, (error as any).response?.data || (error as any).message); + throw error; + } } } diff --git a/packages/toolbox-core/src/toolbox_core/protocol.ts b/packages/toolbox-core/src/toolbox_core/protocol.ts new file mode 100644 index 0000000..ccc7a83 --- /dev/null +++ b/packages/toolbox-core/src/toolbox_core/protocol.ts @@ -0,0 +1,130 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { z, ZodRawShape, ZodTypeAny, ZodObject } from 'zod'; + +// Define All Interfaces + +interface BaseParameter { + name: string; + description: string; + authSources?: string[]; +} + +interface StringParameter extends BaseParameter { + type: "string"; +} + +interface IntegerParameter extends BaseParameter { + type: "integer"; +} + +interface FloatParameter extends BaseParameter { + type: "float"; +} + +interface BooleanParameter extends BaseParameter { + type: "boolean"; +} + +interface ArrayParameter extends BaseParameter { + type: "array"; + items: ParameterSchema; // Recursive reference to the ParameterSchema type +} + +type ParameterSchema = + | StringParameter | IntegerParameter | FloatParameter | BooleanParameter | ArrayParameter; + + +// Get all Zod schema types + +const ZodBaseParameter = z.object({ + name: z.string().min(1, "Parameter name cannot be empty"), + description: z.string(), + authSources: z.array(z.string()).optional(), +}); + + +export const ZodParameterSchema: z.ZodType = z.lazy(() => + z.discriminatedUnion('type', [ + ZodBaseParameter.extend({ + type: z.literal('string'), + }), + ZodBaseParameter.extend({ + type: z.literal('integer'), + }), + ZodBaseParameter.extend({ + type: z.literal('float'), + }), + ZodBaseParameter.extend({ + type: z.literal('boolean'), + }), + ZodBaseParameter.extend({ + type: z.literal('array'), + items: ZodParameterSchema, // Recursive reference for the item's definition + }), + ]) +); + +export const ZodToolSchema = z.object({ + description: z.string().min(1, "Tool description cannot be empty"), + parameters: z.array(ZodParameterSchema), + authRequired: z.array(z.string()).optional(), +}); + +export const ZodManifestSchema = z.object({ + serverVersion: z.string().min(1, "Server version cannot be empty"), + tools: z.record( + z.string().min(1, "Tool name cannot be empty"), ZodToolSchema + ), +}); + +/** + * Recursively builds a Zod schema for a single parameter based on its TypeScript definition. + * @param param The ParameterSchema (TypeScript type) to convert. + * @returns A ZodTypeAny representing the schema for this parameter. + */ +function buildZodShapeFromParameter(param: ParameterSchema): ZodTypeAny { + switch (param.type) { + case "string": + return z.string(); // Consider adding more constraints if available in ParameterSchema + case "integer": + return z.number().int(); + case "float": + return z.number(); + case "boolean": + return z.boolean(); + case "array": + // Recursively build the schema for array items + return z.array(buildZodShapeFromParameter(param.items)); + default: + // This ensures exhaustiveness at compile time if ParameterSchema is a discriminated union + const _exhaustiveCheck: never = param; + throw new Error(`Unknown parameter type: ${(_exhaustiveCheck as any).type}`); + } +} + +/** + * Creates a ZodObject schema from an array of ParameterSchema (TypeScript types). + * This combined schema is used by ToolboxTool to validate its call arguments. + * @param params Array of ParameterSchema objects. + * @returns A ZodObject schema. + */ +export function createZodObjectSchemaFromParameters(params: ParameterSchema[]): ZodObject { + const shape: ZodRawShape = {}; + for (const param of params) { + shape[param.name] = buildZodShapeFromParameter(param); + } + return z.object(shape); +} \ No newline at end of file diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index 2cc72d6..f9252ee 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -12,21 +12,50 @@ // See the License for the specific language governing permissions and // limitations under the License. -function ToolboxTool(name: string) { - const callable = async function (action: {a: number; b: number}) { - // MOCK API CALL - async function api_resp(a: number, b: number): Promise { - await new Promise(resolve => setTimeout(resolve, 10)); - return a + 2 * b; +import { ZodObject, ZodError } from 'zod'; +import { AxiosInstance, AxiosResponse } from 'axios'; + + +function ToolboxTool(session: AxiosInstance, baseUrl: string, name: string, description: string, paramSchema: ZodObject) { + const toolUrl = `${baseUrl}/api/tool/${name}/invoke`; + + const callable = async function (callArguments: Record = {}) { + let validatedPayload: Record; + try { + validatedPayload = paramSchema.parse(callArguments); + } catch (error) { + if (error instanceof ZodError) { + const errorMessages = error.errors.map( + (e) => `${e.path.join(".") || "payload"}: ${e.message}` + ); + throw new Error( + `Argument validation failed for tool "${name}":\n - ${errorMessages.join("\n - ")}` + ); + } + throw new Error(`Argument validation failed: ${String(error)}`); + } + + try { + const response: AxiosResponse = await session.post(toolUrl, validatedPayload); + return response.data; + } catch (error) { + console.error(`Error posting data to ${toolUrl}:`, (error as any).response?.data || (error as any).message); + throw error; } - const result = await api_resp(action.a, action.b); - return result; }; callable.toolName = name; + callable.description = description; + callable.params = paramSchema; callable.getName = function () { return this.toolName; }; + callable.getDescription = function () { + return this.getDescription; + } + callable.getParamSchema = function () { + return this.params; + } return callable; } diff --git a/packages/toolbox-core/src/toolbox_core/utils.ts b/packages/toolbox-core/src/toolbox_core/utils.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts new file mode 100644 index 0000000..2bf7d67 --- /dev/null +++ b/packages/toolbox-core/test/test.client.ts @@ -0,0 +1,21 @@ +import {ToolboxClient} from '../src/toolbox_core/client'; + +const client = new ToolboxClient('http://127.0.0.1:5000'); + +describe('loadTool', () => { + test('Should load a tool', async () => { + const tool = await client.loadTool("search-hotels-by-name") + const toolResp = await tool({name: "Basel"}) + // expect(toolResp).toEqual("Hotel Basel Basel"); + const respStr = JSON.stringify(toolResp) + expect(respStr).toMatch("Holiday Inn Basel") +})}); + + +// async function main() { +// const client = new ToolboxClient("http://127.0.0.1:5000") +// const tool = await client.loadTool("search-hotels-by-name") +// console.log(tool) +// } +// main() + diff --git a/packages/toolbox-core/test/test.e2e.ts b/packages/toolbox-core/test/test.e2e.ts new file mode 100644 index 0000000..485d31a --- /dev/null +++ b/packages/toolbox-core/test/test.e2e.ts @@ -0,0 +1,12 @@ +import {ToolboxClient} from '../src/toolbox_core/client'; + +const client = new ToolboxClient('http://127.0.0.1:5000'); + +describe('loadTool', () => { + test('Should load a tool', async () => { + const tool = await client.loadTool("search-hotels-by-name") + const toolResp = await tool({name: "Basel"}) + // expect(toolResp).toEqual("Hotel Basel Basel"); + const respStr = JSON.stringify(toolResp) + expect(respStr).toMatch("Holiday Inn Basel") +})}); diff --git a/packages/toolbox-core/test/test_client.ts b/packages/toolbox-core/test/test_client.ts deleted file mode 100644 index 0bddb12..0000000 --- a/packages/toolbox-core/test/test_client.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {ToolboxClient} from '../src/toolbox_core/client'; - -const client = new ToolboxClient('https://some_base_url'); - -describe('getToolResponse', () => { - test('Should return a specific value based on inputs', async () => { - const response = await client.getToolResponse(3, 4); - expect(response).toBe(11); - }); -}); From 51474aba38c152c94bc74579fa82f58a705c8d7b Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Fri, 16 May 2025 21:02:39 +0530 Subject: [PATCH 02/57] cleanup --- packages/toolbox-core/src/toolbox_core/client.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index ab9c2e4..7b8c572 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -38,8 +38,6 @@ class ToolboxClient { const response: AxiosResponse = await this._session.get(url); const responseData = response.data; - console.log("DEBUGggggg, response data", responseData) - const manifestResponse = ZodManifestSchema.safeParse(responseData); if (manifestResponse.success) { const manifest = manifestResponse.data; From 813406f1d065448f97eef397ef0d96813c7bb1b5 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Fri, 16 May 2025 21:02:50 +0530 Subject: [PATCH 03/57] clean --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 42b113b..68221a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -*node_modules* \ No newline at end of file +*node_modules* +*build* \ No newline at end of file From 91dc7ec41e93ee758cbf5418b2a05e4780cfe202 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Fri, 16 May 2025 21:23:04 +0530 Subject: [PATCH 04/57] add unit tests --- packages/toolbox-core/jest.config.json | 10 +- .../toolbox-core/src/toolbox_core/protocol.ts | 2 +- packages/toolbox-core/test/test.protocol.ts | 339 ++++++++++++++++++ 3 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 packages/toolbox-core/test/test.protocol.ts diff --git a/packages/toolbox-core/jest.config.json b/packages/toolbox-core/jest.config.json index 07012ac..811fe00 100644 --- a/packages/toolbox-core/jest.config.json +++ b/packages/toolbox-core/jest.config.json @@ -3,5 +3,13 @@ "/test/**" ], "preset": "ts-jest", - "testEnvironment": "node" + "testEnvironment": "node", + "collectCoverage": true, + "coverageDirectory": "coverage", + "coverageReporters": ["lcov", "text"], + "collectCoverageFrom": [ + "src/**/*.ts", + "!src/**/*.d.ts", + "!src/**/index.ts" + ] } \ No newline at end of file diff --git a/packages/toolbox-core/src/toolbox_core/protocol.ts b/packages/toolbox-core/src/toolbox_core/protocol.ts index ccc7a83..0d52efe 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.ts +++ b/packages/toolbox-core/src/toolbox_core/protocol.ts @@ -126,5 +126,5 @@ export function createZodObjectSchemaFromParameters(params: ParameterSchema[]): for (const param of params) { shape[param.name] = buildZodShapeFromParameter(param); } - return z.object(shape); + return z.object(shape).strict(); } \ No newline at end of file diff --git a/packages/toolbox-core/test/test.protocol.ts b/packages/toolbox-core/test/test.protocol.ts new file mode 100644 index 0000000..a0e2e7d --- /dev/null +++ b/packages/toolbox-core/test/test.protocol.ts @@ -0,0 +1,339 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ZodError } from 'zod'; +import { + ZodParameterSchema, + ZodToolSchema, + ZodManifestSchema, + createZodObjectSchemaFromParameters, +} from '../src/toolbox_core/protocol'; + +// Helper function to get Zod errors for easier assertions +const getErrorMessages = (error: ZodError) => { + return error.errors.map((e) => { + if (e.path.length > 0) { + return `${e.path.join('.')}: ${e.message}`; + } + return e.message; + }); +}; + +describe('ZodParameterSchema', () => { + it('should validate a correct string parameter', () => { + const data = { name: 'testString', description: 'A string', type: 'string' }; + expect(ZodParameterSchema.safeParse(data).success).toBe(true); + }); + + it('should validate a string parameter with authSources', () => { + const data = { name: 'testString', description: 'A string', type: 'string', authSources: ['google', 'custom'] }; + expect(ZodParameterSchema.safeParse(data).success).toBe(true); + }); + + it('should invalidate a string parameter with an empty name', () => { + const data = { name: '', description: 'A string', type: 'string' }; + const result = ZodParameterSchema.safeParse(data); + expect(result.success).toBe(false); + if (!result.success) { + expect(getErrorMessages(result.error)).toContain('name: Parameter name cannot be empty'); + } + }); + + it('should validate a correct integer parameter', () => { + const data = { name: 'testInt', description: 'An integer', type: 'integer' }; + expect(ZodParameterSchema.safeParse(data).success).toBe(true); + }); + + it('should validate a correct float parameter', () => { + const data = { name: 'testFloat', description: 'A float', type: 'float' }; + expect(ZodParameterSchema.safeParse(data).success).toBe(true); + }); + + it('should validate a correct boolean parameter', () => { + const data = { name: 'testBool', description: 'A boolean', type: 'boolean' }; + expect(ZodParameterSchema.safeParse(data).success).toBe(true); + }); + + it('should validate a correct array parameter with string items', () => { + const data = { + name: 'testArray', + description: 'An array of strings', + type: 'array', + items: { name: 'item_name', description: 'item_desc', type: 'string' }, + }; + expect(ZodParameterSchema.safeParse(data).success).toBe(true); + }); + + it('should validate a correct array parameter with integer items', () => { + const data = { + name: 'testArrayInt', + description: 'An array of integers', + type: 'array', + items: { name: 'int_item', description: 'item_desc', type: 'integer' }, + }; + expect(ZodParameterSchema.safeParse(data).success).toBe(true); + }); + + + it('should validate a nested array parameter', () => { + const data = { + name: 'outerArray', + description: 'Outer array', + type: 'array', + items: { + name: 'innerArray', + description: 'Inner array of integers', + type: 'array', + items: { name: 'intItem', description: 'integer item', type: 'integer' }, + }, + }; + const result = ZodParameterSchema.safeParse(data); + expect(result.success).toBe(true); + }); + + it('should invalidate an array parameter with missing items definition', () => { + const data = { + name: 'testArray', + description: 'An array', + type: 'array', + // items is missing + }; + const result = ZodParameterSchema.safeParse(data); + expect(result.success).toBe(false); + if (!result.success) { + expect(getErrorMessages(result.error)).toEqual( + expect.arrayContaining([expect.stringMatching(/items: Required/i)]) + ); + } + }); + + it('should invalidate an array parameter with item having an empty name', () => { + const data = { + name: 'testArray', + description: 'An array', + type: 'array', + items: { name: '', description: 'item desc', type: 'string' }, + }; + const result = ZodParameterSchema.safeParse(data); + expect(result.success).toBe(false); + if (!result.success) { + expect(getErrorMessages(result.error)).toContain('items.name: Parameter name cannot be empty'); + } + }); + + it('should invalidate if type is missing', () => { + const data = { name: 'testParam', description: 'A param' }; + const result = ZodParameterSchema.safeParse(data); + expect(result.success).toBe(false); + if (!result.success) { + expect(getErrorMessages(result.error)).toEqual( + expect.arrayContaining([expect.stringMatching(/Invalid discriminator value/i)]) + ); + } + }); +}); + +describe('ZodToolSchema', () => { + const validParameter = { name: 'param1', description: 'String param', type: 'string' as const }; + + it('should validate a correct tool schema', () => { + const data = { + description: 'My test tool', + parameters: [validParameter], + }; + expect(ZodToolSchema.safeParse(data).success).toBe(true); + }); + + it('should validate a tool schema with authRequired', () => { + const data = { + description: 'My auth tool', + parameters: [], + authRequired: ['google_oauth'], + }; + expect(ZodToolSchema.safeParse(data).success).toBe(true); + }); + + it('should invalidate a tool schema with an empty description', () => { + const data = { + description: '', + parameters: [validParameter], + }; + const result = ZodToolSchema.safeParse(data); + expect(result.success).toBe(false); + if (!result.success) { + expect(getErrorMessages(result.error)).toContain('description: Tool description cannot be empty'); + } + }); + + it('should invalidate a tool schema with an invalid parameter', () => { + const data = { + description: 'My test tool', + parameters: [{ name: '', description: 'Empty name param', type: 'string' }], // Invalid parameter + }; + const result = ZodToolSchema.safeParse(data); + expect(result.success).toBe(false); + if (!result.success) { + expect(getErrorMessages(result.error)).toContain('parameters.0.name: Parameter name cannot be empty'); + } + }); +}); + +describe('ZodManifestSchema', () => { + const validTool = { + description: 'Tool A does something', + parameters: [{ name: 'input', description: 'input string', type: 'string' as const }], + }; + + it('should validate a correct manifest schema', () => { + const data = { + serverVersion: '1.0.0', + tools: { + toolA: validTool, + toolB: { + description: 'Tool B does something else', + parameters: [{ name: 'count', description: 'count number', type: 'integer' as const }], + authRequired: ['admin'], + }, + }, + }; + expect(ZodManifestSchema.safeParse(data).success).toBe(true); + }); + + it('should invalidate a manifest schema with an empty serverVersion', () => { + const data = { + serverVersion: '', + tools: { toolA: validTool }, + }; + const result = ZodManifestSchema.safeParse(data); + expect(result.success).toBe(false); + if (!result.success) { + expect(getErrorMessages(result.error)).toContain('serverVersion: Server version cannot be empty'); + } + }); + + it('should invalidate a manifest schema with an empty tool name', () => { + const data = { + serverVersion: '1.0.0', + tools: { + '': validTool, // Empty tool name + }, + }; + const result = ZodManifestSchema.safeParse(data); + expect(result.success).toBe(false); + if (!result.success) { + expect(getErrorMessages(result.error)).toEqual( + expect.arrayContaining([expect.stringMatching(/Tool name cannot be empty/i)]) + ); + } + }); + + it('should invalidate a manifest schema with an invalid tool structure', () => { + const data = { + serverVersion: '1.0.0', + tools: { + toolA: { + description: '', + parameters: [], + }, + }, + }; + const result = ZodManifestSchema.safeParse(data); + expect(result.success).toBe(false); + if (!result.success) { + expect(getErrorMessages(result.error)).toContain('tools.toolA.description: Tool description cannot be empty'); + } + }); +}); + +describe('createZodObjectSchemaFromParameters', () => { + it('should create an empty Zod object for an empty parameters array', () => { + const params: any[] = []; + const schema = createZodObjectSchemaFromParameters(params); + expect(schema.safeParse({}).success).toBe(true); + expect(schema.safeParse({ anyKey: 'anyValue' }).success).toBe(false); // Strict object + }); + + it('should create a Zod object schema from mixed parameter types', () => { + const params = [ + { name: 'username', description: 'User login name', type: 'string' as const }, + { name: 'age', description: 'User age', type: 'integer' as const }, + { name: 'isActive', description: 'User status', type: 'boolean' as const }, + ]; + const schema = createZodObjectSchemaFromParameters(params); + + const validData = { username: 'john_doe', age: 30, isActive: true }; + expect(schema.safeParse(validData).success).toBe(true); + + const invalidData1 = { username: 'john_doe', age: '30', isActive: true }; // age as string + const result1 = schema.safeParse(invalidData1); + expect(result1.success).toBe(false); + if (!result1.success) expect(getErrorMessages(result1.error)).toContain('age: Expected number, received string'); + + const invalidData2 = { username: 'john_doe', isActive: true }; // missing age + const result2 = schema.safeParse(invalidData2); + expect(result2.success).toBe(false); + if (!result2.success) expect(getErrorMessages(result2.error)).toContain('age: Required'); + }); + + it('should create a Zod object schema with an array parameter', () => { + const params = [ + { + name: 'tags', + description: 'List of tags', + type: 'array' as const, + items: { name: 'tag_item', description: 'A tag', type: 'string' as const }, + }, + { name: 'id', description: 'd', type: 'integer' as const} + ]; + const schema = createZodObjectSchemaFromParameters(params); + + const validData = { tags: ['news', 'tech'], id: 1 }; + expect(schema.safeParse(validData).success).toBe(true); + + const invalidData = { tags: ['news', 123], id: 1 }; // number in string array + const result = schema.safeParse(invalidData); + expect(result.success).toBe(false); + if (!result.success) { + // The error path will be tags.1 (for the second item in the array) + expect(getErrorMessages(result.error)).toContain('tags.1: Expected string, received number'); + } + }); + + it('should create a Zod object schema with a nested array parameter', () => { + const params = [ + { + name: 'matrix', + description: 'A matrix of numbers', + type: 'array' as const, + items: { + name: 'row', + description: 'A row in the matrix', + type: 'array' as const, + items: { name: 'cell', description: 'A cell value', type: 'float' as const }, + }, + }, + ]; + const schema = createZodObjectSchemaFromParameters(params); + + const validData = { matrix: [[1.0, 2.5], [3.0, 4.5]] }; + expect(schema.safeParse(validData).success).toBe(true); + + const invalidData = { matrix: [[1.0, '2.5'], [3.0, 4.5]] }; // string in float array + const result = schema.safeParse(invalidData); + expect(result.success).toBe(false); + if(!result.success) { + expect(getErrorMessages(result.error)).toContain('matrix.0.1: Expected number, received string'); + } + }); +}); \ No newline at end of file From dc3968295558ed4a15e3a7a7272ec4cc2910577d Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Fri, 16 May 2025 21:25:26 +0530 Subject: [PATCH 05/57] increase test cov --- packages/toolbox-core/test/test.protocol.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/toolbox-core/test/test.protocol.ts b/packages/toolbox-core/test/test.protocol.ts index a0e2e7d..b4c473d 100644 --- a/packages/toolbox-core/test/test.protocol.ts +++ b/packages/toolbox-core/test/test.protocol.ts @@ -336,4 +336,17 @@ describe('createZodObjectSchemaFromParameters', () => { expect(getErrorMessages(result.error)).toContain('matrix.0.1: Expected number, received string'); } }); + + it('should throw an error for an unknown parameter type in buildZodShapeFromParameter', () => { + const paramsWithUnknownType = [ + { + name: 'faultyParam', + description: 'This param has a type that buildZodShapeFromParameter does not handle', + type: 'someUnrecognizedType' as any, // Forcing an invalid type + }, + ]; + expect(() => createZodObjectSchemaFromParameters(paramsWithUnknownType)).toThrow( + 'Unknown parameter type: someUnrecognizedType' + ); + }); }); \ No newline at end of file From 17400570199c03468cdd8dc330481bebcda04b22 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Fri, 16 May 2025 21:31:03 +0530 Subject: [PATCH 06/57] test file cleanup --- packages/toolbox-core/test/test.protocol.ts | 343 +++++++++----------- 1 file changed, 154 insertions(+), 189 deletions(-) diff --git a/packages/toolbox-core/test/test.protocol.ts b/packages/toolbox-core/test/test.protocol.ts index b4c473d..05fca8d 100644 --- a/packages/toolbox-core/test/test.protocol.ts +++ b/packages/toolbox-core/test/test.protocol.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ZodError } from 'zod'; +import { ZodError, ZodTypeAny } from 'zod'; import { ZodParameterSchema, ZodToolSchema, @@ -20,102 +20,111 @@ import { createZodObjectSchemaFromParameters, } from '../src/toolbox_core/protocol'; -// Helper function to get Zod errors for easier assertions -const getErrorMessages = (error: ZodError) => { - return error.errors.map((e) => { - if (e.path.length > 0) { - return `${e.path.join('.')}: ${e.message}`; - } - return e.message; - }); -}; +// HELPER FUNCTIONS -describe('ZodParameterSchema', () => { - it('should validate a correct string parameter', () => { - const data = { name: 'testString', description: 'A string', type: 'string' }; - expect(ZodParameterSchema.safeParse(data).success).toBe(true); - }); - - it('should validate a string parameter with authSources', () => { - const data = { name: 'testString', description: 'A string', type: 'string', authSources: ['google', 'custom'] }; - expect(ZodParameterSchema.safeParse(data).success).toBe(true); - }); - - it('should invalidate a string parameter with an empty name', () => { - const data = { name: '', description: 'A string', type: 'string' }; - const result = ZodParameterSchema.safeParse(data); +const getErrorMessages = (error: ZodError): string[] => { + return error.errors.map((e) => { + if (e.path.length > 0) { + return `${e.path.join('.')}: ${e.message}`; + } + return e.message; + }); +}; + +const expectParseSuccess = (schema: ZodTypeAny, data: unknown) => { + const result = schema.safeParse(data); + expect(result.success).toBe(true); +}; + +const expectParseFailure = ( + schema: ZodTypeAny, + data: unknown, + errorMessageCheck: (errors: string[]) => void +) => { + const result = schema.safeParse(data); expect(result.success).toBe(false); + if (!result.success) { - expect(getErrorMessages(result.error)).toContain('name: Parameter name cannot be empty'); + errorMessageCheck(getErrorMessages(result.error)); + } else { + fail(`Parsing was expected to fail for ${JSON.stringify(data)} but succeeded.`); } - }); - - it('should validate a correct integer parameter', () => { - const data = { name: 'testInt', description: 'An integer', type: 'integer' }; - expect(ZodParameterSchema.safeParse(data).success).toBe(true); - }); - - it('should validate a correct float parameter', () => { - const data = { name: 'testFloat', description: 'A float', type: 'float' }; - expect(ZodParameterSchema.safeParse(data).success).toBe(true); - }); +}; - it('should validate a correct boolean parameter', () => { - const data = { name: 'testBool', description: 'A boolean', type: 'boolean' }; - expect(ZodParameterSchema.safeParse(data).success).toBe(true); - }); +// TESTS - it('should validate a correct array parameter with string items', () => { - const data = { - name: 'testArray', - description: 'An array of strings', - type: 'array', - items: { name: 'item_name', description: 'item_desc', type: 'string' }, - }; - expect(ZodParameterSchema.safeParse(data).success).toBe(true); - }); +describe('ZodParameterSchema', () => { + const validParameterTestCases = [ + { + description: 'correct string parameter', + data: { name: 'testString', description: 'A string', type: 'string' }, + }, + { + description: 'string parameter with authSources', + data: { name: 'testString', description: 'A string', type: 'string', authSources: ['google', 'custom'] }, + }, + { + description: 'correct integer parameter', + data: { name: 'testInt', description: 'An integer', type: 'integer' }, + }, + { + description: 'correct float parameter', + data: { name: 'testFloat', description: 'A float', type: 'float' }, + }, + { + description: 'correct boolean parameter', + data: { name: 'testBool', description: 'A boolean', type: 'boolean' }, + }, + { + description: 'correct array parameter with string items', + data: { + name: 'testArray', + description: 'An array of strings', + type: 'array', + items: { name: 'item_name', description: 'item_desc', type: 'string' }, + }, + }, + { + description: 'correct array parameter with integer items', + data: { + name: 'testArrayInt', + description: 'An array of integers', + type: 'array', + items: { name: 'int_item', description: 'item_desc', type: 'integer' }, + }, + }, + { + description: 'nested array parameter', + data: { + name: 'outerArray', + description: 'Outer array', + type: 'array', + items: { + name: 'innerArray', + description: 'Inner array of integers', + type: 'array', + items: { name: 'intItem', description: 'integer item', type: 'integer' }, + }, + }, + }, + ]; - it('should validate a correct array parameter with integer items', () => { - const data = { - name: 'testArrayInt', - description: 'An array of integers', - type: 'array', - items: { name: 'int_item', description: 'item_desc', type: 'integer' }, - }; - expect(ZodParameterSchema.safeParse(data).success).toBe(true); + test.each(validParameterTestCases)('should validate a $description', ({ data }) => { + expectParseSuccess(ZodParameterSchema, data); }); - - it('should validate a nested array parameter', () => { - const data = { - name: 'outerArray', - description: 'Outer array', - type: 'array', - items: { - name: 'innerArray', - description: 'Inner array of integers', - type: 'array', - items: { name: 'intItem', description: 'integer item', type: 'integer' }, - }, - }; - const result = ZodParameterSchema.safeParse(data); - expect(result.success).toBe(true); + it('should invalidate a string parameter with an empty name', () => { + const data = { name: '', description: 'A string', type: 'string' }; + expectParseFailure(ZodParameterSchema, data, (errors) => { + expect(errors).toContain('name: Parameter name cannot be empty'); + }); }); it('should invalidate an array parameter with missing items definition', () => { - const data = { - name: 'testArray', - description: 'An array', - type: 'array', - // items is missing - }; - const result = ZodParameterSchema.safeParse(data); - expect(result.success).toBe(false); - if (!result.success) { - expect(getErrorMessages(result.error)).toEqual( - expect.arrayContaining([expect.stringMatching(/items: Required/i)]) - ); - } + const data = { name: 'testArray', description: 'An array', type: 'array' }; + expectParseFailure(ZodParameterSchema, data, (errors) => { + expect(errors).toEqual(expect.arrayContaining([expect.stringMatching(/items: Required/i)])); + }); }); it('should invalidate an array parameter with item having an empty name', () => { @@ -125,22 +134,16 @@ describe('ZodParameterSchema', () => { type: 'array', items: { name: '', description: 'item desc', type: 'string' }, }; - const result = ZodParameterSchema.safeParse(data); - expect(result.success).toBe(false); - if (!result.success) { - expect(getErrorMessages(result.error)).toContain('items.name: Parameter name cannot be empty'); - } + expectParseFailure(ZodParameterSchema, data, (errors) => { + expect(errors).toContain('items.name: Parameter name cannot be empty'); + }); }); - it('should invalidate if type is missing', () => { - const data = { name: 'testParam', description: 'A param' }; - const result = ZodParameterSchema.safeParse(data); - expect(result.success).toBe(false); - if (!result.success) { - expect(getErrorMessages(result.error)).toEqual( - expect.arrayContaining([expect.stringMatching(/Invalid discriminator value/i)]) - ); - } + it('should invalidate if type is missing', () => { + const data = { name: 'testParam', description: 'A param' }; // type is missing + expectParseFailure(ZodParameterSchema, data, (errors) => { + expect(errors).toEqual(expect.arrayContaining([expect.stringMatching(/Invalid discriminator value/i)])); + }); }); }); @@ -152,7 +155,7 @@ describe('ZodToolSchema', () => { description: 'My test tool', parameters: [validParameter], }; - expect(ZodToolSchema.safeParse(data).success).toBe(true); + expectParseSuccess(ZodToolSchema, data); }); it('should validate a tool schema with authRequired', () => { @@ -161,31 +164,24 @@ describe('ZodToolSchema', () => { parameters: [], authRequired: ['google_oauth'], }; - expect(ZodToolSchema.safeParse(data).success).toBe(true); + expectParseSuccess(ZodToolSchema, data); }); it('should invalidate a tool schema with an empty description', () => { - const data = { - description: '', - parameters: [validParameter], - }; - const result = ZodToolSchema.safeParse(data); - expect(result.success).toBe(false); - if (!result.success) { - expect(getErrorMessages(result.error)).toContain('description: Tool description cannot be empty'); - } + const data = { description: '', parameters: [validParameter] }; + expectParseFailure(ZodToolSchema, data, (errors) => { + expect(errors).toContain('description: Tool description cannot be empty'); + }); }); it('should invalidate a tool schema with an invalid parameter', () => { const data = { description: 'My test tool', - parameters: [{ name: '', description: 'Empty name param', type: 'string' }], // Invalid parameter + parameters: [{ name: '', description: 'Empty name param', type: 'string' }], }; - const result = ZodToolSchema.safeParse(data); - expect(result.success).toBe(false); - if (!result.success) { - expect(getErrorMessages(result.error)).toContain('parameters.0.name: Parameter name cannot be empty'); - } + expectParseFailure(ZodToolSchema, data, (errors) => { + expect(errors).toContain('parameters.0.name: Parameter name cannot be empty'); + }); }); }); @@ -207,111 +203,84 @@ describe('ZodManifestSchema', () => { }, }, }; - expect(ZodManifestSchema.safeParse(data).success).toBe(true); + expectParseSuccess(ZodManifestSchema, data); }); it('should invalidate a manifest schema with an empty serverVersion', () => { - const data = { - serverVersion: '', - tools: { toolA: validTool }, - }; - const result = ZodManifestSchema.safeParse(data); - expect(result.success).toBe(false); - if (!result.success) { - expect(getErrorMessages(result.error)).toContain('serverVersion: Server version cannot be empty'); - } + const data = { serverVersion: '', tools: { toolA: validTool } }; + expectParseFailure(ZodManifestSchema, data, (errors) => { + expect(errors).toContain('serverVersion: Server version cannot be empty'); + }); }); it('should invalidate a manifest schema with an empty tool name', () => { - const data = { - serverVersion: '1.0.0', - tools: { - '': validTool, // Empty tool name - }, - }; - const result = ZodManifestSchema.safeParse(data); - expect(result.success).toBe(false); - if (!result.success) { - expect(getErrorMessages(result.error)).toEqual( - expect.arrayContaining([expect.stringMatching(/Tool name cannot be empty/i)]) - ); - } + const data = { serverVersion: '1.0.0', tools: { '': validTool } }; + expectParseFailure(ZodManifestSchema, data, (errors) => { + expect(errors).toEqual(expect.arrayContaining([expect.stringMatching(/Tool name cannot be empty/i)])); + }); }); it('should invalidate a manifest schema with an invalid tool structure', () => { const data = { serverVersion: '1.0.0', - tools: { - toolA: { - description: '', - parameters: [], - }, - }, + tools: { toolA: { description: '', parameters: [] } }, }; - const result = ZodManifestSchema.safeParse(data); - expect(result.success).toBe(false); - if (!result.success) { - expect(getErrorMessages(result.error)).toContain('tools.toolA.description: Tool description cannot be empty'); - } + expectParseFailure(ZodManifestSchema, data, (errors) => { + expect(errors).toContain('tools.toolA.description: Tool description cannot be empty'); + }); }); }); describe('createZodObjectSchemaFromParameters', () => { - it('should create an empty Zod object for an empty parameters array', () => { + it('should create an empty Zod object for an empty parameters array (and be strict)', () => { const params: any[] = []; const schema = createZodObjectSchemaFromParameters(params); - expect(schema.safeParse({}).success).toBe(true); - expect(schema.safeParse({ anyKey: 'anyValue' }).success).toBe(false); // Strict object + + expectParseSuccess(schema, {}); + expectParseFailure(schema, { anyKey: 'anyValue' }, (errors) => { + expect(errors.some(e => /Unrecognized key\(s\) in object: 'anyKey'/.test(e))).toBe(true); + }); }); - it('should create a Zod object schema from mixed parameter types', () => { - const params = [ + it('should create a Zod object schema from mixed parameter types and validate data', () => { + const params: any[] = [ { name: 'username', description: 'User login name', type: 'string' as const }, { name: 'age', description: 'User age', type: 'integer' as const }, { name: 'isActive', description: 'User status', type: 'boolean' as const }, ]; const schema = createZodObjectSchemaFromParameters(params); - const validData = { username: 'john_doe', age: 30, isActive: true }; - expect(schema.safeParse(validData).success).toBe(true); - - const invalidData1 = { username: 'john_doe', age: '30', isActive: true }; // age as string - const result1 = schema.safeParse(invalidData1); - expect(result1.success).toBe(false); - if (!result1.success) expect(getErrorMessages(result1.error)).toContain('age: Expected number, received string'); + expectParseSuccess(schema, { username: 'john_doe', age: 30, isActive: true }); - const invalidData2 = { username: 'john_doe', isActive: true }; // missing age - const result2 = schema.safeParse(invalidData2); - expect(result2.success).toBe(false); - if (!result2.success) expect(getErrorMessages(result2.error)).toContain('age: Required'); + expectParseFailure(schema, { username: 'john_doe', age: '30', isActive: true }, (errors) => + expect(errors).toContain('age: Expected number, received string') + ); + expectParseFailure(schema, { username: 'john_doe', isActive: true }, (errors) => + expect(errors).toContain('age: Required') + ); }); it('should create a Zod object schema with an array parameter', () => { - const params = [ + const params: any[] = [ { name: 'tags', description: 'List of tags', type: 'array' as const, items: { name: 'tag_item', description: 'A tag', type: 'string' as const }, }, - { name: 'id', description: 'd', type: 'integer' as const} + { name: 'id', description: 'An identifier', type: 'integer' as const }, ]; const schema = createZodObjectSchemaFromParameters(params); - const validData = { tags: ['news', 'tech'], id: 1 }; - expect(schema.safeParse(validData).success).toBe(true); + expectParseSuccess(schema, { tags: ['news', 'tech'], id: 1 }); - const invalidData = { tags: ['news', 123], id: 1 }; // number in string array - const result = schema.safeParse(invalidData); - expect(result.success).toBe(false); - if (!result.success) { - // The error path will be tags.1 (for the second item in the array) - expect(getErrorMessages(result.error)).toContain('tags.1: Expected string, received number'); - } + expectParseFailure(schema, { tags: ['news', 123], id: 1 }, (errors) => { + expect(errors).toContain('tags.1: Expected string, received number'); + }); }); it('should create a Zod object schema with a nested array parameter', () => { - const params = [ + const params: any[] = [ { name: 'matrix', description: 'A matrix of numbers', @@ -326,22 +295,18 @@ describe('createZodObjectSchemaFromParameters', () => { ]; const schema = createZodObjectSchemaFromParameters(params); - const validData = { matrix: [[1.0, 2.5], [3.0, 4.5]] }; - expect(schema.safeParse(validData).success).toBe(true); + expectParseSuccess(schema, { matrix: [[1.0, 2.5], [3.0, 4.5]] }); - const invalidData = { matrix: [[1.0, '2.5'], [3.0, 4.5]] }; // string in float array - const result = schema.safeParse(invalidData); - expect(result.success).toBe(false); - if(!result.success) { - expect(getErrorMessages(result.error)).toContain('matrix.0.1: Expected number, received string'); - } + expectParseFailure(schema, { matrix: [[1.0, '2.5'], [3.0, 4.5]] }, (errors) => { + expect(errors).toContain('matrix.0.1: Expected number, received string'); + }); }); - it('should throw an error for an unknown parameter type in buildZodShapeFromParameter', () => { - const paramsWithUnknownType = [ + it('should throw an error when creating schema from parameter with unknown type', () => { + const paramsWithUnknownType: any[] = [ { name: 'faultyParam', - description: 'This param has a type that buildZodShapeFromParameter does not handle', + description: 'This param has an unhandled type', type: 'someUnrecognizedType' as any, // Forcing an invalid type }, ]; From b9cf84fe71aebb431604eea0433db6fd1b3616f1 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Fri, 16 May 2025 21:31:17 +0530 Subject: [PATCH 07/57] clean --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 68221a2..0962617 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *node_modules* -*build* \ No newline at end of file +*build* +*coverage* \ No newline at end of file From 3beed73065aadf9b5fb7adceb04f06dbf0aabb9f Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Fri, 16 May 2025 21:37:00 +0530 Subject: [PATCH 08/57] small fix --- packages/toolbox-core/src/toolbox_core/tool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index f9252ee..7b737e6 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -51,7 +51,7 @@ function ToolboxTool(session: AxiosInstance, baseUrl: string, name: string, desc return this.toolName; }; callable.getDescription = function () { - return this.getDescription; + return this.description; } callable.getParamSchema = function () { return this.params; From a5a6955af93cf90b4e3d3bbc706f59ca13725e9c Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Fri, 16 May 2025 21:45:05 +0530 Subject: [PATCH 09/57] Added unit tests for tool --- packages/toolbox-core/test/test.tool.ts | 210 ++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 packages/toolbox-core/test/test.tool.ts diff --git a/packages/toolbox-core/test/test.tool.ts b/packages/toolbox-core/test/test.tool.ts new file mode 100644 index 0000000..f1a238e --- /dev/null +++ b/packages/toolbox-core/test/test.tool.ts @@ -0,0 +1,210 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ToolboxTool } from '../src/toolbox_core/tool'; +import { z, ZodObject } from 'zod'; +import { AxiosInstance, AxiosResponse } from 'axios'; + +// Global mocks for Axios +const mockAxiosPost = jest.fn(); +const mockSession = { + post: mockAxiosPost, +} as unknown as AxiosInstance; + +// Helper to create structured API error objects for testing +type ApiErrorWithMessage = Error & { response?: { data: any } }; +const createApiError = (message: string, responseData?: any): ApiErrorWithMessage => { + const error = new Error(message) as ApiErrorWithMessage; + if (responseData !== undefined) { + error.response = { data: responseData }; + } + return error; +}; + +describe('ToolboxTool', () => { + // Common constants for the tool + const baseURL = 'http://api.example.com'; + const toolName = 'myTestTool'; + const toolDescription = 'This is a description for the test tool.'; + + // Variables to be initialized in beforeEach + let basicParamSchema: ZodObject; + let consoleErrorSpy: jest.SpyInstance; + let tool: ReturnType; + + beforeEach(() => { + // Reset mocks before each test + mockAxiosPost.mockReset(); + + // Initialize a basic schema used by many tests + basicParamSchema = z.object({ + query: z.string().min(1, "Query cannot be empty"), + limit: z.number().optional(), + }); + + // Spy on console.error to prevent logging and allow assertions + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + // Restore the original console.error + consoleErrorSpy.mockRestore(); + }); + + describe('Factory Properties and Getters', () => { + beforeEach(() => { + tool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, basicParamSchema); + }); + + it('should correctly assign toolName, description, and params to the callable function', () => { + expect(tool.toolName).toBe(toolName); + expect(tool.description).toBe(toolDescription); + expect(tool.params).toBe(basicParamSchema); + }); + + it('getName() should return the tool name', () => { + expect(tool.getName()).toBe(toolName); + }); + + it('getDescription() should return the tool description', () => { + expect(tool.getDescription()).toBe(toolDescription); + }); + + it('getParamSchema() should return the parameter schema', () => { + expect(tool.getParamSchema()).toBe(basicParamSchema); + }); + }); + + describe('Callable Function - Argument Validation', () => { + it('should call paramSchema.parse with the provided arguments', async () => { + const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, basicParamSchema); + const parseSpy = jest.spyOn(basicParamSchema, 'parse'); + const callArgs = { query: 'test query' }; + mockAxiosPost.mockResolvedValueOnce({ data: 'success' } as AxiosResponse); + + await currentTool(callArgs); + + expect(parseSpy).toHaveBeenCalledWith(callArgs); + parseSpy.mockRestore(); + }); + + it('should throw a formatted ZodError if argument validation fails', async () => { + const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, basicParamSchema); + const invalidArgs = { query: '' }; // Fails min(1) constraint + + await expect(currentTool(invalidArgs)).rejects.toThrow( + `Argument validation failed for tool "${toolName}":\n - query: Query cannot be empty` + ); + expect(mockAxiosPost).not.toHaveBeenCalled(); + }); + + it('should handle multiple ZodError issues in the validation error message', async () => { + const complexSchema = z.object({ + name: z.string().min(1, "Name is required"), + age: z.number().positive("Age must be positive"), + }); + const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, complexSchema); + const invalidArgs = { name: '', age: -5 }; + + await expect(currentTool(invalidArgs)).rejects.toThrowError( + expect.stringMatching( + new RegExp(`Argument validation failed for tool "${toolName}":` + + `(\\n - name: Name is required|\\n - age: Age must be positive){2}`) + ) + ); + expect(mockAxiosPost).not.toHaveBeenCalled(); + }); + + it('should throw a generic error if paramSchema.parse throws a non-ZodError', async () => { + const customError = new Error('A non-Zod parsing error occurred!'); + const failingSchema = { + parse: jest.fn().mockImplementation(() => { throw customError; }), + } as unknown as ZodObject; + const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, failingSchema); + const callArgs = { query: 'some query' }; + + await expect(currentTool(callArgs)).rejects.toThrowError( + `Argument validation failed: ${String(customError)}` + ); + expect(mockAxiosPost).not.toHaveBeenCalled(); + }); + + it('should use an empty object as default if no arguments are provided and schema allows it', async () => { + const emptySchema = z.object({}); + const parseSpy = jest.spyOn(emptySchema, 'parse'); + const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, emptySchema); + mockAxiosPost.mockResolvedValueOnce({ data: 'success' }); + + await currentTool(); + + expect(parseSpy).toHaveBeenCalledWith({}); + expect(mockAxiosPost).toHaveBeenCalled(); + parseSpy.mockRestore(); + }); + + it('should fail validation if no arguments are given and schema requires them', async () => { + const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, basicParamSchema); + await expect(currentTool()).rejects.toThrow( + /Argument validation failed for tool "myTestTool":\n - query: Required/ + ); + expect(mockAxiosPost).not.toHaveBeenCalled(); + }); + }); + + describe('Callable Function - API Call Execution', () => { + const validArgs = { query: 'search term', limit: 10 }; + const expectedUrl = `${baseURL}/api/tool/${toolName}/invoke`; + const mockApiResponseData = { result: 'Data from API' }; + + beforeEach(() => { + tool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, basicParamSchema); + }); + + it('should make a POST request to the correct URL with the validated payload', async () => { + mockAxiosPost.mockResolvedValueOnce({ data: mockApiResponseData } as AxiosResponse); + + const result = await tool(validArgs); + + expect(mockAxiosPost).toHaveBeenCalledTimes(1); + expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); + expect(result).toEqual(mockApiResponseData); + }); + + it('should re-throw the error and log to console.error if API call fails with response data', async () => { + const apiErrorResponseData = { error: 'detail from server' }; + const apiError = createApiError('API request failed', apiErrorResponseData); + mockAxiosPost.mockRejectedValueOnce(apiError); + + await expect(tool(validArgs)).rejects.toThrow(apiError); + expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error posting data to ${expectedUrl}:`, + apiErrorResponseData + ); + }); + + it('should re-throw the error and log (using error.message) if API call fails without response data', async () => { + const apiErrorMessage = 'Network connection refused'; + const apiError = createApiError(apiErrorMessage); // No responseData + mockAxiosPost.mockRejectedValueOnce(apiError); + + await expect(tool(validArgs)).rejects.toThrow(apiError); + expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error posting data to ${expectedUrl}:`, + apiErrorMessage + ); + }); + }); +}); \ No newline at end of file From af78176bfd8488cafa97df2600c825180387e2bf Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Fri, 16 May 2025 21:46:04 +0530 Subject: [PATCH 10/57] cleanup --- packages/toolbox-core/test/test.tool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/test/test.tool.ts b/packages/toolbox-core/test/test.tool.ts index f1a238e..5ddf920 100644 --- a/packages/toolbox-core/test/test.tool.ts +++ b/packages/toolbox-core/test/test.tool.ts @@ -101,7 +101,7 @@ describe('ToolboxTool', () => { it('should throw a formatted ZodError if argument validation fails', async () => { const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, basicParamSchema); - const invalidArgs = { query: '' }; // Fails min(1) constraint + const invalidArgs = { query: '' }; // Fails because of empty string await expect(currentTool(invalidArgs)).rejects.toThrow( `Argument validation failed for tool "${toolName}":\n - query: Query cannot be empty` From 4875d981c1eb65387a10f0a8774c7a64176cbdee Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Fri, 16 May 2025 22:26:02 +0530 Subject: [PATCH 11/57] test cleanup --- packages/toolbox-core/test/test.tool.ts | 58 +++++++++++++++++-------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/packages/toolbox-core/test/test.tool.ts b/packages/toolbox-core/test/test.tool.ts index 5ddf920..46a1e39 100644 --- a/packages/toolbox-core/test/test.tool.ts +++ b/packages/toolbox-core/test/test.tool.ts @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -103,9 +103,12 @@ describe('ToolboxTool', () => { const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, basicParamSchema); const invalidArgs = { query: '' }; // Fails because of empty string - await expect(currentTool(invalidArgs)).rejects.toThrow( - `Argument validation failed for tool "${toolName}":\n - query: Query cannot be empty` - ); + try { + await currentTool(invalidArgs); + throw new Error(`Expected currentTool to throw a Zod validation error for tool "${toolName}", but it did not.`); + } catch (e: any) { + expect(e.message).toBe(`Argument validation failed for tool "${toolName}":\n - query: Query cannot be empty`); + } expect(mockAxiosPost).not.toHaveBeenCalled(); }); @@ -117,12 +120,14 @@ describe('ToolboxTool', () => { const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, complexSchema); const invalidArgs = { name: '', age: -5 }; - await expect(currentTool(invalidArgs)).rejects.toThrowError( - expect.stringMatching( - new RegExp(`Argument validation failed for tool "${toolName}":` + - `(\\n - name: Name is required|\\n - age: Age must be positive){2}`) - ) - ); + try { + await currentTool(invalidArgs); + throw new Error('Expected currentTool to throw a Zod validation error, but it did not.'); + } catch (e: any) { + expect(e.message).toEqual(expect.stringContaining(`Argument validation failed for tool "${toolName}":`)); + expect(e.message).toEqual(expect.stringContaining("name: Name is required")); + expect(e.message).toEqual(expect.stringContaining("age: Age must be positive")); + } expect(mockAxiosPost).not.toHaveBeenCalled(); }); @@ -134,9 +139,12 @@ describe('ToolboxTool', () => { const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, failingSchema); const callArgs = { query: 'some query' }; - await expect(currentTool(callArgs)).rejects.toThrowError( - `Argument validation failed: ${String(customError)}` - ); + try { + await currentTool(callArgs); + throw new Error('Expected currentTool to throw a non-Zod error during parsing, but it did not.'); + } catch (e: any) { + expect(e.message).toBe(`Argument validation failed: ${String(customError)}`); + } expect(mockAxiosPost).not.toHaveBeenCalled(); }); @@ -155,9 +163,13 @@ describe('ToolboxTool', () => { it('should fail validation if no arguments are given and schema requires them', async () => { const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, basicParamSchema); - await expect(currentTool()).rejects.toThrow( - /Argument validation failed for tool "myTestTool":\n - query: Required/ - ); + try { + await currentTool(); + throw new Error(`Expected currentTool to throw a Zod validation error for tool "${toolName}" when no args provided, but it did not.`); + } catch (e: any) { + expect(e.message).toEqual(expect.stringContaining(`Argument validation failed for tool "myTestTool":`)); + expect(e.message).toEqual(expect.stringContaining("query: Required")); + } expect(mockAxiosPost).not.toHaveBeenCalled(); }); }); @@ -186,7 +198,12 @@ describe('ToolboxTool', () => { const apiError = createApiError('API request failed', apiErrorResponseData); mockAxiosPost.mockRejectedValueOnce(apiError); - await expect(tool(validArgs)).rejects.toThrow(apiError); + try { + await tool(validArgs); + throw new Error('Expected tool call to throw an API error with response data, but it did not.'); + } catch (e: any) { + expect(e).toBe(apiError); + } expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); expect(consoleErrorSpy).toHaveBeenCalledWith( `Error posting data to ${expectedUrl}:`, @@ -199,7 +216,12 @@ describe('ToolboxTool', () => { const apiError = createApiError(apiErrorMessage); // No responseData mockAxiosPost.mockRejectedValueOnce(apiError); - await expect(tool(validArgs)).rejects.toThrow(apiError); + try { + await tool(validArgs); + throw new Error('Expected tool call to throw an API error without response data, but it did not.'); + } catch (e: any) { + expect(e).toBe(apiError); + } expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); expect(consoleErrorSpy).toHaveBeenCalledWith( `Error posting data to ${expectedUrl}:`, From 30d41e177c02d0c1b98b5ffe36676f59edfa134f Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Fri, 16 May 2025 22:26:26 +0530 Subject: [PATCH 12/57] new tests --- packages/toolbox-core/test/test.client.ts | 221 ++++++++++++++++++++-- 1 file changed, 205 insertions(+), 16 deletions(-) diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 2bf7d67..68afb13 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -1,21 +1,210 @@ -import {ToolboxClient} from '../src/toolbox_core/client'; +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -const client = new ToolboxClient('http://127.0.0.1:5000'); +import { ToolboxClient } from '../src/toolbox_core/client'; +import { ToolboxTool } from '../src/toolbox_core/tool'; +import { ZodManifestSchema, createZodObjectSchemaFromParameters } from '../src/toolbox_core/protocol'; +import axios, { AxiosInstance, AxiosResponse } from 'axios'; -describe('loadTool', () => { - test('Should load a tool', async () => { - const tool = await client.loadTool("search-hotels-by-name") - const toolResp = await tool({name: "Basel"}) - // expect(toolResp).toEqual("Hotel Basel Basel"); - const respStr = JSON.stringify(toolResp) - expect(respStr).toMatch("Holiday Inn Basel") -})}); +// --- Mocking External Dependencies --- +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; +jest.mock('../src/toolbox_core/tool', () => ({ + ToolboxTool: jest.fn(), +})); +const MockedToolboxToolFactory = ToolboxTool as jest.MockedFunction; -// async function main() { -// const client = new ToolboxClient("http://127.0.0.1:5000") -// const tool = await client.loadTool("search-hotels-by-name") -// console.log(tool) -// } -// main() +jest.mock('../src/toolbox_core/protocol', () => ({ + ZodManifestSchema: { + safeParse: jest.fn(), + }, + createZodObjectSchemaFromParameters: jest.fn(), +})); +const MockedZodManifestSchema = ZodManifestSchema as jest.Mocked; +const MockedCreateZodObjectSchemaFromParameters = createZodObjectSchemaFromParameters as jest.MockedFunction; +// --- Test Helper Functions --- +type ApiErrorWithMessage = Error & { response?: { data: any } }; +const createApiError = (message: string, responseData?: any): ApiErrorWithMessage => { + const error = new Error(message) as ApiErrorWithMessage; + if (responseData !== undefined) { + error.response = { data: responseData }; + } + return error; +}; + +describe('ToolboxClient', () => { + const testBaseUrl = 'http://api.example.com'; + let consoleErrorSpy: jest.SpyInstance; + let mockSessionGet: jest.Mock; + + beforeEach(() => { + jest.resetAllMocks(); + + mockSessionGet = jest.fn(); + mockedAxios.create.mockReturnValue({ + get: mockSessionGet, + } as unknown as AxiosInstance); + + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + describe('constructor', () => { + it('should set baseUrl and create a new session if one is not provided', () => { + const client = new ToolboxClient(testBaseUrl); + + expect((client as any)._baseUrl).toBe(testBaseUrl); + expect(mockedAxios.create).toHaveBeenCalledTimes(1); + expect(mockedAxios.create).toHaveBeenCalledWith({ baseURL: testBaseUrl }); + expect((client as any)._session.get).toBe(mockSessionGet); + }); + + it('should set baseUrl and use the provided session if one is given', () => { + const customMockSession = { get: mockSessionGet } as unknown as AxiosInstance; + const client = new ToolboxClient(testBaseUrl, customMockSession); + + expect((client as any)._baseUrl).toBe(testBaseUrl); + expect((client as any)._session).toBe(customMockSession); + expect(mockedAxios.create).not.toHaveBeenCalled(); + }); + }); + + describe('loadTool', () => { + const toolName = 'calculator'; + const expectedApiUrl = `${testBaseUrl}/api/tool/${toolName}`; + let client: ToolboxClient; + + beforeEach(() => { + client = new ToolboxClient(testBaseUrl); + }); + + const setupMocksForSuccessfulLoad = (toolDefinition: object, overrides: Partial<{ + manifestData: object; + zodParamsSchema: object; + toolInstance: object; + }> = {}) => { + const manifestData = overrides.manifestData || { + serverVersion: '1.0.0', + tools: { [toolName]: toolDefinition }, + }; + const zodParamsSchema = overrides.zodParamsSchema || { _isMockZodParamSchema: true, forTool: toolName }; + const toolInstance = overrides.toolInstance || { _isMockTool: true, loadedName: toolName }; + + mockSessionGet.mockResolvedValueOnce({ data: manifestData } as AxiosResponse); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: manifestData } as any); + MockedCreateZodObjectSchemaFromParameters.mockReturnValueOnce(zodParamsSchema as any); + MockedToolboxToolFactory.mockReturnValueOnce(toolInstance as any); + + return { manifestData, zodParamsSchema, toolInstance }; + }; + + + it('should successfully load a tool with valid manifest and API response', async () => { + const mockToolDefinition = { + description: 'Performs calculations', + parameters: [{ name: 'expression', type: 'string', description: 'Math expression' }], + }; + + const { zodParamsSchema, toolInstance, manifestData } = setupMocksForSuccessfulLoad(mockToolDefinition); + const loadedTool = await client.loadTool(toolName); + + expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); + expect(MockedZodManifestSchema.safeParse).toHaveBeenCalledWith(manifestData); + expect(MockedCreateZodObjectSchemaFromParameters).toHaveBeenCalledWith(mockToolDefinition.parameters); + expect(MockedToolboxToolFactory).toHaveBeenCalledWith( + (client as any)._session, + testBaseUrl, + toolName, + mockToolDefinition.description, + zodParamsSchema + ); + expect(loadedTool).toBe(toolInstance); + }); + + it('should throw an error if manifest parsing fails', async () => { + const mockApiResponseData = { invalid: 'manifest structure' }; + const mockZodErrorDetail = { message: 'Zod validation failed on manifest' }; + mockSessionGet.mockResolvedValueOnce({ data: mockApiResponseData } as AxiosResponse); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: false, error: mockZodErrorDetail } as any); + + await expect(client.loadTool(toolName)).rejects.toThrow( + `Invalid manifest structure received: ${mockZodErrorDetail.message}` + ); + expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); + expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); + }); + + it('should throw an error if manifest.tools key is missing', async () => { + const mockManifestWithoutTools = { serverVersion: '1.0.0' }; // 'tools' key absent + setupMocksForSuccessfulLoad({description: '', parameters: []}, {manifestData: mockManifestWithoutTools}); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: mockManifestWithoutTools } as any); + + + await expect(client.loadTool(toolName)).rejects.toThrow( + `Tool "${toolName}" not found in manifest.` + ); + expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); + expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); + }); + + it('should throw an error if the specific tool is not found in manifest.tools', async () => { + const mockManifestWithOtherTools = { + serverVersion: '1.0.0', + tools: { anotherTool: { description: 'A different tool', parameters: [] } }, + }; + mockSessionGet.mockResolvedValueOnce({ data: mockManifestWithOtherTools } as AxiosResponse); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: mockManifestWithOtherTools } as any); + + + await expect(client.loadTool(toolName)).rejects.toThrow( + `Tool "${toolName}" not found in manifest.` + ); + expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); + expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); + }); + + it('should throw and log error if API GET request fails with response data', async () => { + const errorResponseData = { code: 500, message: 'Server-side issue' }; + const apiError = createApiError('API call failed unexpectedly', errorResponseData); + mockSessionGet.mockRejectedValueOnce(apiError); + + await expect(client.loadTool(toolName)).rejects.toThrow(apiError); + expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error fetching data from ${expectedApiUrl}:`, + errorResponseData + ); + expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); + }); + + it('should throw and log error (using error.message) if API GET request fails without response data', async () => { + const errorMessage = 'Network unavailable'; + const apiError = createApiError(errorMessage); + mockSessionGet.mockRejectedValueOnce(apiError); + + await expect(client.loadTool(toolName)).rejects.toThrow(apiError); + expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error fetching data from ${expectedApiUrl}:`, + errorMessage + ); + expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); + }); + }); +}); From d4fa91f0e2130bcde64dea5ff64c25de6ba64417 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Fri, 16 May 2025 22:26:40 +0530 Subject: [PATCH 13/57] del e2e file --- packages/toolbox-core/test/test.e2e.ts | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 packages/toolbox-core/test/test.e2e.ts diff --git a/packages/toolbox-core/test/test.e2e.ts b/packages/toolbox-core/test/test.e2e.ts deleted file mode 100644 index 485d31a..0000000 --- a/packages/toolbox-core/test/test.e2e.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {ToolboxClient} from '../src/toolbox_core/client'; - -const client = new ToolboxClient('http://127.0.0.1:5000'); - -describe('loadTool', () => { - test('Should load a tool', async () => { - const tool = await client.loadTool("search-hotels-by-name") - const toolResp = await tool({name: "Basel"}) - // expect(toolResp).toEqual("Hotel Basel Basel"); - const respStr = JSON.stringify(toolResp) - expect(respStr).toMatch("Holiday Inn Basel") -})}); From 3fbdaf08b5e22743c0fbde6db21bcfd2bb4d85e3 Mon Sep 17 00:00:00 2001 From: Twisha Bansal <58483338+twishabansal@users.noreply.github.com> Date: Mon, 19 May 2025 18:42:53 +0530 Subject: [PATCH 14/57] chore: lint (#11) * lint * lint * lint --- .../toolbox-core/src/toolbox_core/client.ts | 36 ++- .../toolbox-core/src/toolbox_core/protocol.ts | 80 +++--- .../toolbox-core/src/toolbox_core/tool.ts | 32 ++- packages/toolbox-core/test/test.client.ts | 148 +++++++---- packages/toolbox-core/test/test.protocol.ts | 235 ++++++++++++------ packages/toolbox-core/test/test.tool.ts | 172 +++++++++---- 6 files changed, 473 insertions(+), 230 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index 7b8c572..69da949 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -13,19 +13,23 @@ // limitations under the License. import {ToolboxTool} from './tool'; -import axios, { AxiosInstance, AxiosResponse } from 'axios'; -import {ZodManifestSchema, createZodObjectSchemaFromParameters} from './protocol'; +import axios from 'axios'; +import type {AxiosInstance, AxiosResponse} from 'axios'; +import { + ZodManifestSchema, + createZodObjectSchemaFromParameters, +} from './protocol'; class ToolboxClient { - /** @private */ _baseUrl; - /** @private */ _session; + /** @private */ private _baseUrl: string; + /** @private */ private _session: AxiosInstance; /** * @param {string} url - The base URL for the Toolbox service API. */ constructor(url: string, session?: AxiosInstance) { this._baseUrl = url; - this._session = session || axios.create({ baseURL: url }); + this._session = session || axios.create({baseURL: url}); } /** @@ -33,7 +37,7 @@ class ToolboxClient { * @returns {ToolboxTool} - A ToolboxTool instance. */ async loadTool(toolName: string): Promise> { - const url = `${this._baseUrl}/api/tool/${toolName}` + const url = `${this._baseUrl}/api/tool/${toolName}`; try { const response: AxiosResponse = await this._session.get(url); const responseData = response.data; @@ -41,10 +45,14 @@ class ToolboxClient { const manifestResponse = ZodManifestSchema.safeParse(responseData); if (manifestResponse.success) { const manifest = manifestResponse.data; - if (manifest.tools && manifest.tools.hasOwnProperty(toolName)) { + if ( + manifest.tools && + Object.prototype.hasOwnProperty.call(manifest.tools, toolName) + ) { const specificToolSchema = manifest.tools[toolName]; - const paramZodSchema = createZodObjectSchemaFromParameters(specificToolSchema.parameters) - + const paramZodSchema = createZodObjectSchemaFromParameters( + specificToolSchema.parameters + ); return ToolboxTool( this._session, this._baseUrl, @@ -56,11 +64,15 @@ class ToolboxClient { throw new Error(`Tool "${toolName}" not found in manifest.`); } } else { - throw new Error(`Invalid manifest structure received: ${manifestResponse.error.message}`); + throw new Error( + `Invalid manifest structure received: ${manifestResponse.error.message}` + ); } - } catch (error) { - console.error(`Error fetching data from ${url}:`, (error as any).response?.data || (error as any).message); + console.error( + `Error fetching data from ${url}:`, + (error as any).response?.data || (error as any).message + ); throw error; } } diff --git a/packages/toolbox-core/src/toolbox_core/protocol.ts b/packages/toolbox-core/src/toolbox_core/protocol.ts index 0d52efe..89b83b5 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.ts +++ b/packages/toolbox-core/src/toolbox_core/protocol.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { z, ZodRawShape, ZodTypeAny, ZodObject } from 'zod'; +import {z, ZodRawShape, ZodTypeAny, ZodObject} from 'zod'; // Define All Interfaces @@ -23,39 +23,41 @@ interface BaseParameter { } interface StringParameter extends BaseParameter { - type: "string"; + type: 'string'; } interface IntegerParameter extends BaseParameter { - type: "integer"; + type: 'integer'; } interface FloatParameter extends BaseParameter { - type: "float"; + type: 'float'; } interface BooleanParameter extends BaseParameter { - type: "boolean"; + type: 'boolean'; } interface ArrayParameter extends BaseParameter { - type: "array"; + type: 'array'; items: ParameterSchema; // Recursive reference to the ParameterSchema type } type ParameterSchema = - | StringParameter | IntegerParameter | FloatParameter | BooleanParameter | ArrayParameter; - + | StringParameter + | IntegerParameter + | FloatParameter + | BooleanParameter + | ArrayParameter; // Get all Zod schema types const ZodBaseParameter = z.object({ - name: z.string().min(1, "Parameter name cannot be empty"), + name: z.string().min(1, 'Parameter name cannot be empty'), description: z.string(), authSources: z.array(z.string()).optional(), }); - export const ZodParameterSchema: z.ZodType = z.lazy(() => z.discriminatedUnion('type', [ ZodBaseParameter.extend({ @@ -78,15 +80,16 @@ export const ZodParameterSchema: z.ZodType = z.lazy(() => ); export const ZodToolSchema = z.object({ - description: z.string().min(1, "Tool description cannot be empty"), + description: z.string().min(1, 'Tool description cannot be empty'), parameters: z.array(ZodParameterSchema), authRequired: z.array(z.string()).optional(), }); export const ZodManifestSchema = z.object({ - serverVersion: z.string().min(1, "Server version cannot be empty"), + serverVersion: z.string().min(1, 'Server version cannot be empty'), tools: z.record( - z.string().min(1, "Tool name cannot be empty"), ZodToolSchema + z.string().min(1, 'Tool name cannot be empty'), + ZodToolSchema ), }); @@ -96,23 +99,26 @@ export const ZodManifestSchema = z.object({ * @returns A ZodTypeAny representing the schema for this parameter. */ function buildZodShapeFromParameter(param: ParameterSchema): ZodTypeAny { - switch (param.type) { - case "string": - return z.string(); // Consider adding more constraints if available in ParameterSchema - case "integer": - return z.number().int(); - case "float": - return z.number(); - case "boolean": - return z.boolean(); - case "array": - // Recursively build the schema for array items - return z.array(buildZodShapeFromParameter(param.items)); - default: - // This ensures exhaustiveness at compile time if ParameterSchema is a discriminated union - const _exhaustiveCheck: never = param; - throw new Error(`Unknown parameter type: ${(_exhaustiveCheck as any).type}`); + switch (param.type) { + case 'string': + return z.string(); + case 'integer': + return z.number().int(); + case 'float': + return z.number(); + case 'boolean': + return z.boolean(); + case 'array': + // Recursively build the schema for array items + return z.array(buildZodShapeFromParameter(param.items)); + default: { + // This ensures exhaustiveness at compile time if ParameterSchema is a discriminated union + const _exhaustiveCheck: never = param; + throw new Error( + `Unknown parameter type: ${(_exhaustiveCheck as any).type}` + ); } + } } /** @@ -121,10 +127,12 @@ function buildZodShapeFromParameter(param: ParameterSchema): ZodTypeAny { * @param params Array of ParameterSchema objects. * @returns A ZodObject schema. */ -export function createZodObjectSchemaFromParameters(params: ParameterSchema[]): ZodObject { - const shape: ZodRawShape = {}; - for (const param of params) { - shape[param.name] = buildZodShapeFromParameter(param); - } - return z.object(shape).strict(); -} \ No newline at end of file +export function createZodObjectSchemaFromParameters( + params: ParameterSchema[] +): ZodObject { + const shape: ZodRawShape = {}; + for (const param of params) { + shape[param.name] = buildZodShapeFromParameter(param); + } + return z.object(shape).strict(); +} diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index 7b737e6..fecd911 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -12,11 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ZodObject, ZodError } from 'zod'; -import { AxiosInstance, AxiosResponse } from 'axios'; +import {ZodObject, ZodError} from 'zod'; +import {AxiosInstance, AxiosResponse} from 'axios'; - -function ToolboxTool(session: AxiosInstance, baseUrl: string, name: string, description: string, paramSchema: ZodObject) { +function ToolboxTool( + session: AxiosInstance, + baseUrl: string, + name: string, + description: string, + paramSchema: ZodObject +) { const toolUrl = `${baseUrl}/api/tool/${name}/invoke`; const callable = async function (callArguments: Record = {}) { @@ -26,23 +31,28 @@ function ToolboxTool(session: AxiosInstance, baseUrl: string, name: string, desc } catch (error) { if (error instanceof ZodError) { const errorMessages = error.errors.map( - (e) => `${e.path.join(".") || "payload"}: ${e.message}` + e => `${e.path.join('.') || 'payload'}: ${e.message}` ); throw new Error( - `Argument validation failed for tool "${name}":\n - ${errorMessages.join("\n - ")}` + `Argument validation failed for tool "${name}":\n - ${errorMessages.join('\n - ')}` ); } throw new Error(`Argument validation failed: ${String(error)}`); } try { - const response: AxiosResponse = await session.post(toolUrl, validatedPayload); + const response: AxiosResponse = await session.post( + toolUrl, + validatedPayload + ); return response.data; } catch (error) { - console.error(`Error posting data to ${toolUrl}:`, (error as any).response?.data || (error as any).message); + console.error( + `Error posting data to ${toolUrl}:`, + (error as any).response?.data || (error as any).message + ); throw error; } - }; callable.toolName = name; callable.description = description; @@ -52,10 +62,10 @@ function ToolboxTool(session: AxiosInstance, baseUrl: string, name: string, desc }; callable.getDescription = function () { return this.description; - } + }; callable.getParamSchema = function () { return this.params; - } + }; return callable; } diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 68afb13..3120841 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -12,10 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ToolboxClient } from '../src/toolbox_core/client'; -import { ToolboxTool } from '../src/toolbox_core/tool'; -import { ZodManifestSchema, createZodObjectSchemaFromParameters } from '../src/toolbox_core/protocol'; -import axios, { AxiosInstance, AxiosResponse } from 'axios'; +import {ToolboxClient} from '../src/toolbox_core/client'; +import {ToolboxTool} from '../src/toolbox_core/tool'; +import { + ZodManifestSchema, + createZodObjectSchemaFromParameters, +} from '../src/toolbox_core/protocol'; +import axios, {AxiosInstance, AxiosResponse} from 'axios'; // --- Mocking External Dependencies --- jest.mock('axios'); @@ -24,7 +27,9 @@ const mockedAxios = axios as jest.Mocked; jest.mock('../src/toolbox_core/tool', () => ({ ToolboxTool: jest.fn(), })); -const MockedToolboxToolFactory = ToolboxTool as jest.MockedFunction; +const MockedToolboxToolFactory = ToolboxTool as jest.MockedFunction< + typeof ToolboxTool +>; jest.mock('../src/toolbox_core/protocol', () => ({ ZodManifestSchema: { @@ -32,15 +37,23 @@ jest.mock('../src/toolbox_core/protocol', () => ({ }, createZodObjectSchemaFromParameters: jest.fn(), })); -const MockedZodManifestSchema = ZodManifestSchema as jest.Mocked; -const MockedCreateZodObjectSchemaFromParameters = createZodObjectSchemaFromParameters as jest.MockedFunction; +const MockedZodManifestSchema = ZodManifestSchema as jest.Mocked< + typeof ZodManifestSchema +>; +const MockedCreateZodObjectSchemaFromParameters = + createZodObjectSchemaFromParameters as jest.MockedFunction< + typeof createZodObjectSchemaFromParameters + >; // --- Test Helper Functions --- -type ApiErrorWithMessage = Error & { response?: { data: any } }; -const createApiError = (message: string, responseData?: any): ApiErrorWithMessage => { +type ApiErrorWithMessage = Error & {response?: {data: any}}; +const createApiError = ( + message: string, + responseData?: any +): ApiErrorWithMessage => { const error = new Error(message) as ApiErrorWithMessage; if (responseData !== undefined) { - error.response = { data: responseData }; + error.response = {data: responseData}; } return error; }; @@ -68,15 +81,17 @@ describe('ToolboxClient', () => { describe('constructor', () => { it('should set baseUrl and create a new session if one is not provided', () => { const client = new ToolboxClient(testBaseUrl); - + expect((client as any)._baseUrl).toBe(testBaseUrl); expect(mockedAxios.create).toHaveBeenCalledTimes(1); - expect(mockedAxios.create).toHaveBeenCalledWith({ baseURL: testBaseUrl }); + expect(mockedAxios.create).toHaveBeenCalledWith({baseURL: testBaseUrl}); expect((client as any)._session.get).toBe(mockSessionGet); }); it('should set baseUrl and use the provided session if one is given', () => { - const customMockSession = { get: mockSessionGet } as unknown as AxiosInstance; + const customMockSession = { + get: mockSessionGet, + } as unknown as AxiosInstance; const client = new ToolboxClient(testBaseUrl, customMockSession); expect((client as any)._baseUrl).toBe(testBaseUrl); @@ -94,39 +109,61 @@ describe('ToolboxClient', () => { client = new ToolboxClient(testBaseUrl); }); - const setupMocksForSuccessfulLoad = (toolDefinition: object, overrides: Partial<{ + const setupMocksForSuccessfulLoad = ( + toolDefinition: object, + overrides: Partial<{ manifestData: object; zodParamsSchema: object; toolInstance: object; - }> = {}) => { - const manifestData = overrides.manifestData || { - serverVersion: '1.0.0', - tools: { [toolName]: toolDefinition }, - }; - const zodParamsSchema = overrides.zodParamsSchema || { _isMockZodParamSchema: true, forTool: toolName }; - const toolInstance = overrides.toolInstance || { _isMockTool: true, loadedName: toolName }; - - mockSessionGet.mockResolvedValueOnce({ data: manifestData } as AxiosResponse); - MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: manifestData } as any); - MockedCreateZodObjectSchemaFromParameters.mockReturnValueOnce(zodParamsSchema as any); - MockedToolboxToolFactory.mockReturnValueOnce(toolInstance as any); - - return { manifestData, zodParamsSchema, toolInstance }; - }; + }> = {} + ) => { + const manifestData = overrides.manifestData || { + serverVersion: '1.0.0', + tools: {[toolName]: toolDefinition}, + }; + const zodParamsSchema = overrides.zodParamsSchema || { + _isMockZodParamSchema: true, + forTool: toolName, + }; + const toolInstance = overrides.toolInstance || { + _isMockTool: true, + loadedName: toolName, + }; + + mockSessionGet.mockResolvedValueOnce({ + data: manifestData, + } as AxiosResponse); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ + success: true, + data: manifestData, + } as any); + MockedCreateZodObjectSchemaFromParameters.mockReturnValueOnce( + zodParamsSchema as any + ); + MockedToolboxToolFactory.mockReturnValueOnce(toolInstance as any); + return {manifestData, zodParamsSchema, toolInstance}; + }; it('should successfully load a tool with valid manifest and API response', async () => { const mockToolDefinition = { description: 'Performs calculations', - parameters: [{ name: 'expression', type: 'string', description: 'Math expression' }], + parameters: [ + {name: 'expression', type: 'string', description: 'Math expression'}, + ], }; - const { zodParamsSchema, toolInstance, manifestData } = setupMocksForSuccessfulLoad(mockToolDefinition); + const {zodParamsSchema, toolInstance, manifestData} = + setupMocksForSuccessfulLoad(mockToolDefinition); const loadedTool = await client.loadTool(toolName); expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); - expect(MockedZodManifestSchema.safeParse).toHaveBeenCalledWith(manifestData); - expect(MockedCreateZodObjectSchemaFromParameters).toHaveBeenCalledWith(mockToolDefinition.parameters); + expect(MockedZodManifestSchema.safeParse).toHaveBeenCalledWith( + manifestData + ); + expect(MockedCreateZodObjectSchemaFromParameters).toHaveBeenCalledWith( + mockToolDefinition.parameters + ); expect(MockedToolboxToolFactory).toHaveBeenCalledWith( (client as any)._session, testBaseUrl, @@ -138,10 +175,15 @@ describe('ToolboxClient', () => { }); it('should throw an error if manifest parsing fails', async () => { - const mockApiResponseData = { invalid: 'manifest structure' }; - const mockZodErrorDetail = { message: 'Zod validation failed on manifest' }; - mockSessionGet.mockResolvedValueOnce({ data: mockApiResponseData } as AxiosResponse); - MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: false, error: mockZodErrorDetail } as any); + const mockApiResponseData = {invalid: 'manifest structure'}; + const mockZodErrorDetail = {message: 'Zod validation failed on manifest'}; + mockSessionGet.mockResolvedValueOnce({ + data: mockApiResponseData, + } as AxiosResponse); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ + success: false, + error: mockZodErrorDetail, + } as any); await expect(client.loadTool(toolName)).rejects.toThrow( `Invalid manifest structure received: ${mockZodErrorDetail.message}` @@ -151,10 +193,15 @@ describe('ToolboxClient', () => { }); it('should throw an error if manifest.tools key is missing', async () => { - const mockManifestWithoutTools = { serverVersion: '1.0.0' }; // 'tools' key absent - setupMocksForSuccessfulLoad({description: '', parameters: []}, {manifestData: mockManifestWithoutTools}); - MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: mockManifestWithoutTools } as any); - + const mockManifestWithoutTools = {serverVersion: '1.0.0'}; // 'tools' key absent + setupMocksForSuccessfulLoad( + {description: '', parameters: []}, + {manifestData: mockManifestWithoutTools} + ); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ + success: true, + data: mockManifestWithoutTools, + } as any); await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` @@ -166,11 +213,15 @@ describe('ToolboxClient', () => { it('should throw an error if the specific tool is not found in manifest.tools', async () => { const mockManifestWithOtherTools = { serverVersion: '1.0.0', - tools: { anotherTool: { description: 'A different tool', parameters: [] } }, + tools: {anotherTool: {description: 'A different tool', parameters: []}}, }; - mockSessionGet.mockResolvedValueOnce({ data: mockManifestWithOtherTools } as AxiosResponse); - MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: mockManifestWithOtherTools } as any); - + mockSessionGet.mockResolvedValueOnce({ + data: mockManifestWithOtherTools, + } as AxiosResponse); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ + success: true, + data: mockManifestWithOtherTools, + } as any); await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` @@ -180,8 +231,11 @@ describe('ToolboxClient', () => { }); it('should throw and log error if API GET request fails with response data', async () => { - const errorResponseData = { code: 500, message: 'Server-side issue' }; - const apiError = createApiError('API call failed unexpectedly', errorResponseData); + const errorResponseData = {code: 500, message: 'Server-side issue'}; + const apiError = createApiError( + 'API call failed unexpectedly', + errorResponseData + ); mockSessionGet.mockRejectedValueOnce(apiError); await expect(client.loadTool(toolName)).rejects.toThrow(apiError); diff --git a/packages/toolbox-core/test/test.protocol.ts b/packages/toolbox-core/test/test.protocol.ts index 05fca8d..6068ad4 100644 --- a/packages/toolbox-core/test/test.protocol.ts +++ b/packages/toolbox-core/test/test.protocol.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ZodError, ZodTypeAny } from 'zod'; +import {ZodError, ZodTypeAny} from 'zod'; import { ZodParameterSchema, ZodToolSchema, @@ -23,32 +23,34 @@ import { // HELPER FUNCTIONS const getErrorMessages = (error: ZodError): string[] => { - return error.errors.map((e) => { - if (e.path.length > 0) { - return `${e.path.join('.')}: ${e.message}`; - } - return e.message; - }); + return error.errors.map(e => { + if (e.path.length > 0) { + return `${e.path.join('.')}: ${e.message}`; + } + return e.message; + }); }; - + const expectParseSuccess = (schema: ZodTypeAny, data: unknown) => { - const result = schema.safeParse(data); - expect(result.success).toBe(true); + const result = schema.safeParse(data); + expect(result.success).toBe(true); }; - + const expectParseFailure = ( - schema: ZodTypeAny, - data: unknown, - errorMessageCheck: (errors: string[]) => void + schema: ZodTypeAny, + data: unknown, + errorMessageCheck: (errors: string[]) => void ) => { - const result = schema.safeParse(data); - expect(result.success).toBe(false); - - if (!result.success) { - errorMessageCheck(getErrorMessages(result.error)); - } else { - fail(`Parsing was expected to fail for ${JSON.stringify(data)} but succeeded.`); - } + const result = schema.safeParse(data); + expect(result.success).toBe(false); + + if (!result.success) { + errorMessageCheck(getErrorMessages(result.error)); + } else { + fail( + `Parsing was expected to fail for ${JSON.stringify(data)} but succeeded.` + ); + } }; // TESTS @@ -57,23 +59,28 @@ describe('ZodParameterSchema', () => { const validParameterTestCases = [ { description: 'correct string parameter', - data: { name: 'testString', description: 'A string', type: 'string' }, + data: {name: 'testString', description: 'A string', type: 'string'}, }, { description: 'string parameter with authSources', - data: { name: 'testString', description: 'A string', type: 'string', authSources: ['google', 'custom'] }, + data: { + name: 'testString', + description: 'A string', + type: 'string', + authSources: ['google', 'custom'], + }, }, { description: 'correct integer parameter', - data: { name: 'testInt', description: 'An integer', type: 'integer' }, + data: {name: 'testInt', description: 'An integer', type: 'integer'}, }, { description: 'correct float parameter', - data: { name: 'testFloat', description: 'A float', type: 'float' }, + data: {name: 'testFloat', description: 'A float', type: 'float'}, }, { description: 'correct boolean parameter', - data: { name: 'testBool', description: 'A boolean', type: 'boolean' }, + data: {name: 'testBool', description: 'A boolean', type: 'boolean'}, }, { description: 'correct array parameter with string items', @@ -81,7 +88,7 @@ describe('ZodParameterSchema', () => { name: 'testArray', description: 'An array of strings', type: 'array', - items: { name: 'item_name', description: 'item_desc', type: 'string' }, + items: {name: 'item_name', description: 'item_desc', type: 'string'}, }, }, { @@ -90,7 +97,7 @@ describe('ZodParameterSchema', () => { name: 'testArrayInt', description: 'An array of integers', type: 'array', - items: { name: 'int_item', description: 'item_desc', type: 'integer' }, + items: {name: 'int_item', description: 'item_desc', type: 'integer'}, }, }, { @@ -103,27 +110,36 @@ describe('ZodParameterSchema', () => { name: 'innerArray', description: 'Inner array of integers', type: 'array', - items: { name: 'intItem', description: 'integer item', type: 'integer' }, + items: { + name: 'intItem', + description: 'integer item', + type: 'integer', + }, }, }, }, ]; - test.each(validParameterTestCases)('should validate a $description', ({ data }) => { - expectParseSuccess(ZodParameterSchema, data); - }); + test.each(validParameterTestCases)( + 'should validate a $description', + ({data}) => { + expectParseSuccess(ZodParameterSchema, data); + } + ); it('should invalidate a string parameter with an empty name', () => { - const data = { name: '', description: 'A string', type: 'string' }; - expectParseFailure(ZodParameterSchema, data, (errors) => { + const data = {name: '', description: 'A string', type: 'string'}; + expectParseFailure(ZodParameterSchema, data, errors => { expect(errors).toContain('name: Parameter name cannot be empty'); }); }); it('should invalidate an array parameter with missing items definition', () => { - const data = { name: 'testArray', description: 'An array', type: 'array' }; - expectParseFailure(ZodParameterSchema, data, (errors) => { - expect(errors).toEqual(expect.arrayContaining([expect.stringMatching(/items: Required/i)])); + const data = {name: 'testArray', description: 'An array', type: 'array'}; + expectParseFailure(ZodParameterSchema, data, errors => { + expect(errors).toEqual( + expect.arrayContaining([expect.stringMatching(/items: Required/i)]) + ); }); }); @@ -132,23 +148,31 @@ describe('ZodParameterSchema', () => { name: 'testArray', description: 'An array', type: 'array', - items: { name: '', description: 'item desc', type: 'string' }, + items: {name: '', description: 'item desc', type: 'string'}, }; - expectParseFailure(ZodParameterSchema, data, (errors) => { + expectParseFailure(ZodParameterSchema, data, errors => { expect(errors).toContain('items.name: Parameter name cannot be empty'); }); }); it('should invalidate if type is missing', () => { - const data = { name: 'testParam', description: 'A param' }; // type is missing - expectParseFailure(ZodParameterSchema, data, (errors) => { - expect(errors).toEqual(expect.arrayContaining([expect.stringMatching(/Invalid discriminator value/i)])); + const data = {name: 'testParam', description: 'A param'}; // type is missing + expectParseFailure(ZodParameterSchema, data, errors => { + expect(errors).toEqual( + expect.arrayContaining([ + expect.stringMatching(/Invalid discriminator value/i), + ]) + ); }); }); }); describe('ZodToolSchema', () => { - const validParameter = { name: 'param1', description: 'String param', type: 'string' as const }; + const validParameter = { + name: 'param1', + description: 'String param', + type: 'string' as const, + }; it('should validate a correct tool schema', () => { const data = { @@ -168,8 +192,8 @@ describe('ZodToolSchema', () => { }); it('should invalidate a tool schema with an empty description', () => { - const data = { description: '', parameters: [validParameter] }; - expectParseFailure(ZodToolSchema, data, (errors) => { + const data = {description: '', parameters: [validParameter]}; + expectParseFailure(ZodToolSchema, data, errors => { expect(errors).toContain('description: Tool description cannot be empty'); }); }); @@ -177,10 +201,12 @@ describe('ZodToolSchema', () => { it('should invalidate a tool schema with an invalid parameter', () => { const data = { description: 'My test tool', - parameters: [{ name: '', description: 'Empty name param', type: 'string' }], + parameters: [{name: '', description: 'Empty name param', type: 'string'}], }; - expectParseFailure(ZodToolSchema, data, (errors) => { - expect(errors).toContain('parameters.0.name: Parameter name cannot be empty'); + expectParseFailure(ZodToolSchema, data, errors => { + expect(errors).toContain( + 'parameters.0.name: Parameter name cannot be empty' + ); }); }); }); @@ -188,7 +214,9 @@ describe('ZodToolSchema', () => { describe('ZodManifestSchema', () => { const validTool = { description: 'Tool A does something', - parameters: [{ name: 'input', description: 'input string', type: 'string' as const }], + parameters: [ + {name: 'input', description: 'input string', type: 'string' as const}, + ], }; it('should validate a correct manifest schema', () => { @@ -198,7 +226,13 @@ describe('ZodManifestSchema', () => { toolA: validTool, toolB: { description: 'Tool B does something else', - parameters: [{ name: 'count', description: 'count number', type: 'integer' as const }], + parameters: [ + { + name: 'count', + description: 'count number', + type: 'integer' as const, + }, + ], authRequired: ['admin'], }, }, @@ -207,26 +241,32 @@ describe('ZodManifestSchema', () => { }); it('should invalidate a manifest schema with an empty serverVersion', () => { - const data = { serverVersion: '', tools: { toolA: validTool } }; - expectParseFailure(ZodManifestSchema, data, (errors) => { + const data = {serverVersion: '', tools: {toolA: validTool}}; + expectParseFailure(ZodManifestSchema, data, errors => { expect(errors).toContain('serverVersion: Server version cannot be empty'); }); }); it('should invalidate a manifest schema with an empty tool name', () => { - const data = { serverVersion: '1.0.0', tools: { '': validTool } }; - expectParseFailure(ZodManifestSchema, data, (errors) => { - expect(errors).toEqual(expect.arrayContaining([expect.stringMatching(/Tool name cannot be empty/i)])); + const data = {serverVersion: '1.0.0', tools: {'': validTool}}; + expectParseFailure(ZodManifestSchema, data, errors => { + expect(errors).toEqual( + expect.arrayContaining([ + expect.stringMatching(/Tool name cannot be empty/i), + ]) + ); }); }); it('should invalidate a manifest schema with an invalid tool structure', () => { const data = { serverVersion: '1.0.0', - tools: { toolA: { description: '', parameters: [] } }, + tools: {toolA: {description: '', parameters: []}}, }; - expectParseFailure(ZodManifestSchema, data, (errors) => { - expect(errors).toContain('tools.toolA.description: Tool description cannot be empty'); + expectParseFailure(ZodManifestSchema, data, errors => { + expect(errors).toContain( + 'tools.toolA.description: Tool description cannot be empty' + ); }); }); }); @@ -237,25 +277,34 @@ describe('createZodObjectSchemaFromParameters', () => { const schema = createZodObjectSchemaFromParameters(params); expectParseSuccess(schema, {}); - expectParseFailure(schema, { anyKey: 'anyValue' }, (errors) => { - expect(errors.some(e => /Unrecognized key\(s\) in object: 'anyKey'/.test(e))).toBe(true); + expectParseFailure(schema, {anyKey: 'anyValue'}, errors => { + expect( + errors.some(e => /Unrecognized key\(s\) in object: 'anyKey'/.test(e)) + ).toBe(true); }); }); it('should create a Zod object schema from mixed parameter types and validate data', () => { const params: any[] = [ - { name: 'username', description: 'User login name', type: 'string' as const }, - { name: 'age', description: 'User age', type: 'integer' as const }, - { name: 'isActive', description: 'User status', type: 'boolean' as const }, + { + name: 'username', + description: 'User login name', + type: 'string' as const, + }, + {name: 'age', description: 'User age', type: 'integer' as const}, + {name: 'isActive', description: 'User status', type: 'boolean' as const}, ]; const schema = createZodObjectSchemaFromParameters(params); - expectParseSuccess(schema, { username: 'john_doe', age: 30, isActive: true }); + expectParseSuccess(schema, {username: 'john_doe', age: 30, isActive: true}); - expectParseFailure(schema, { username: 'john_doe', age: '30', isActive: true }, (errors) => - expect(errors).toContain('age: Expected number, received string') + expectParseFailure( + schema, + {username: 'john_doe', age: '30', isActive: true}, + errors => + expect(errors).toContain('age: Expected number, received string') ); - expectParseFailure(schema, { username: 'john_doe', isActive: true }, (errors) => + expectParseFailure(schema, {username: 'john_doe', isActive: true}, errors => expect(errors).toContain('age: Required') ); }); @@ -266,15 +315,19 @@ describe('createZodObjectSchemaFromParameters', () => { name: 'tags', description: 'List of tags', type: 'array' as const, - items: { name: 'tag_item', description: 'A tag', type: 'string' as const }, + items: { + name: 'tag_item', + description: 'A tag', + type: 'string' as const, + }, }, - { name: 'id', description: 'An identifier', type: 'integer' as const }, + {name: 'id', description: 'An identifier', type: 'integer' as const}, ]; const schema = createZodObjectSchemaFromParameters(params); - expectParseSuccess(schema, { tags: ['news', 'tech'], id: 1 }); + expectParseSuccess(schema, {tags: ['news', 'tech'], id: 1}); - expectParseFailure(schema, { tags: ['news', 123], id: 1 }, (errors) => { + expectParseFailure(schema, {tags: ['news', 123], id: 1}, errors => { expect(errors).toContain('tags.1: Expected string, received number'); }); }); @@ -289,17 +342,37 @@ describe('createZodObjectSchemaFromParameters', () => { name: 'row', description: 'A row in the matrix', type: 'array' as const, - items: { name: 'cell', description: 'A cell value', type: 'float' as const }, + items: { + name: 'cell', + description: 'A cell value', + type: 'float' as const, + }, }, }, ]; const schema = createZodObjectSchemaFromParameters(params); - expectParseSuccess(schema, { matrix: [[1.0, 2.5], [3.0, 4.5]] }); - - expectParseFailure(schema, { matrix: [[1.0, '2.5'], [3.0, 4.5]] }, (errors) => { - expect(errors).toContain('matrix.0.1: Expected number, received string'); + expectParseSuccess(schema, { + matrix: [ + [1.0, 2.5], + [3.0, 4.5], + ], }); + + expectParseFailure( + schema, + { + matrix: [ + [1.0, '2.5'], + [3.0, 4.5], + ], + }, + errors => { + expect(errors).toContain( + 'matrix.0.1: Expected number, received string' + ); + } + ); }); it('should throw an error when creating schema from parameter with unknown type', () => { @@ -310,8 +383,8 @@ describe('createZodObjectSchemaFromParameters', () => { type: 'someUnrecognizedType' as any, // Forcing an invalid type }, ]; - expect(() => createZodObjectSchemaFromParameters(paramsWithUnknownType)).toThrow( - 'Unknown parameter type: someUnrecognizedType' - ); + expect(() => + createZodObjectSchemaFromParameters(paramsWithUnknownType) + ).toThrow('Unknown parameter type: someUnrecognizedType'); }); -}); \ No newline at end of file +}); diff --git a/packages/toolbox-core/test/test.tool.ts b/packages/toolbox-core/test/test.tool.ts index 46a1e39..bebba58 100644 --- a/packages/toolbox-core/test/test.tool.ts +++ b/packages/toolbox-core/test/test.tool.ts @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ToolboxTool } from '../src/toolbox_core/tool'; -import { z, ZodObject } from 'zod'; -import { AxiosInstance, AxiosResponse } from 'axios'; +import {ToolboxTool} from '../src/toolbox_core/tool'; +import {z, ZodObject} from 'zod'; +import {AxiosInstance, AxiosResponse} from 'axios'; // Global mocks for Axios const mockAxiosPost = jest.fn(); @@ -23,11 +23,14 @@ const mockSession = { } as unknown as AxiosInstance; // Helper to create structured API error objects for testing -type ApiErrorWithMessage = Error & { response?: { data: any } }; -const createApiError = (message: string, responseData?: any): ApiErrorWithMessage => { +type ApiErrorWithMessage = Error & {response?: {data: any}}; +const createApiError = ( + message: string, + responseData?: any +): ApiErrorWithMessage => { const error = new Error(message) as ApiErrorWithMessage; if (responseData !== undefined) { - error.response = { data: responseData }; + error.response = {data: responseData}; } return error; }; @@ -49,7 +52,7 @@ describe('ToolboxTool', () => { // Initialize a basic schema used by many tests basicParamSchema = z.object({ - query: z.string().min(1, "Query cannot be empty"), + query: z.string().min(1, 'Query cannot be empty'), limit: z.number().optional(), }); @@ -64,7 +67,13 @@ describe('ToolboxTool', () => { describe('Factory Properties and Getters', () => { beforeEach(() => { - tool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, basicParamSchema); + tool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); }); it('should correctly assign toolName, description, and params to the callable function', () => { @@ -88,10 +97,16 @@ describe('ToolboxTool', () => { describe('Callable Function - Argument Validation', () => { it('should call paramSchema.parse with the provided arguments', async () => { - const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, basicParamSchema); + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); const parseSpy = jest.spyOn(basicParamSchema, 'parse'); - const callArgs = { query: 'test query' }; - mockAxiosPost.mockResolvedValueOnce({ data: 'success' } as AxiosResponse); + const callArgs = {query: 'test query'}; + mockAxiosPost.mockResolvedValueOnce({data: 'success'} as AxiosResponse); await currentTool(callArgs); @@ -100,33 +115,59 @@ describe('ToolboxTool', () => { }); it('should throw a formatted ZodError if argument validation fails', async () => { - const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, basicParamSchema); - const invalidArgs = { query: '' }; // Fails because of empty string + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); + const invalidArgs = {query: ''}; // Fails because of empty string try { await currentTool(invalidArgs); - throw new Error(`Expected currentTool to throw a Zod validation error for tool "${toolName}", but it did not.`); + throw new Error( + `Expected currentTool to throw a Zod validation error for tool "${toolName}", but it did not.` + ); } catch (e: any) { - expect(e.message).toBe(`Argument validation failed for tool "${toolName}":\n - query: Query cannot be empty`); + expect(e.message).toBe( + `Argument validation failed for tool "${toolName}":\n - query: Query cannot be empty` + ); } expect(mockAxiosPost).not.toHaveBeenCalled(); }); it('should handle multiple ZodError issues in the validation error message', async () => { const complexSchema = z.object({ - name: z.string().min(1, "Name is required"), - age: z.number().positive("Age must be positive"), + name: z.string().min(1, 'Name is required'), + age: z.number().positive('Age must be positive'), }); - const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, complexSchema); - const invalidArgs = { name: '', age: -5 }; + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + complexSchema + ); + const invalidArgs = {name: '', age: -5}; try { await currentTool(invalidArgs); - throw new Error('Expected currentTool to throw a Zod validation error, but it did not.'); + throw new Error( + 'Expected currentTool to throw a Zod validation error, but it did not.' + ); } catch (e: any) { - expect(e.message).toEqual(expect.stringContaining(`Argument validation failed for tool "${toolName}":`)); - expect(e.message).toEqual(expect.stringContaining("name: Name is required")); - expect(e.message).toEqual(expect.stringContaining("age: Age must be positive")); + expect(e.message).toEqual( + expect.stringContaining( + `Argument validation failed for tool "${toolName}":` + ) + ); + expect(e.message).toEqual( + expect.stringContaining('name: Name is required') + ); + expect(e.message).toEqual( + expect.stringContaining('age: Age must be positive') + ); } expect(mockAxiosPost).not.toHaveBeenCalled(); }); @@ -134,16 +175,28 @@ describe('ToolboxTool', () => { it('should throw a generic error if paramSchema.parse throws a non-ZodError', async () => { const customError = new Error('A non-Zod parsing error occurred!'); const failingSchema = { - parse: jest.fn().mockImplementation(() => { throw customError; }), + parse: jest.fn().mockImplementation(() => { + throw customError; + }), } as unknown as ZodObject; - const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, failingSchema); - const callArgs = { query: 'some query' }; + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + failingSchema + ); + const callArgs = {query: 'some query'}; try { await currentTool(callArgs); - throw new Error('Expected currentTool to throw a non-Zod error during parsing, but it did not.'); + throw new Error( + 'Expected currentTool to throw a non-Zod error during parsing, but it did not.' + ); } catch (e: any) { - expect(e.message).toBe(`Argument validation failed: ${String(customError)}`); + expect(e.message).toBe( + `Argument validation failed: ${String(customError)}` + ); } expect(mockAxiosPost).not.toHaveBeenCalled(); }); @@ -151,8 +204,14 @@ describe('ToolboxTool', () => { it('should use an empty object as default if no arguments are provided and schema allows it', async () => { const emptySchema = z.object({}); const parseSpy = jest.spyOn(emptySchema, 'parse'); - const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, emptySchema); - mockAxiosPost.mockResolvedValueOnce({ data: 'success' }); + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + emptySchema + ); + mockAxiosPost.mockResolvedValueOnce({data: 'success'}); await currentTool(); @@ -162,29 +221,49 @@ describe('ToolboxTool', () => { }); it('should fail validation if no arguments are given and schema requires them', async () => { - const currentTool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, basicParamSchema); + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); try { await currentTool(); - throw new Error(`Expected currentTool to throw a Zod validation error for tool "${toolName}" when no args provided, but it did not.`); + throw new Error( + `Expected currentTool to throw a Zod validation error for tool "${toolName}" when no args provided, but it did not.` + ); } catch (e: any) { - expect(e.message).toEqual(expect.stringContaining(`Argument validation failed for tool "myTestTool":`)); - expect(e.message).toEqual(expect.stringContaining("query: Required")); + expect(e.message).toEqual( + expect.stringContaining( + 'Argument validation failed for tool "myTestTool":' + ) + ); + expect(e.message).toEqual(expect.stringContaining('query: Required')); } expect(mockAxiosPost).not.toHaveBeenCalled(); }); }); describe('Callable Function - API Call Execution', () => { - const validArgs = { query: 'search term', limit: 10 }; + const validArgs = {query: 'search term', limit: 10}; const expectedUrl = `${baseURL}/api/tool/${toolName}/invoke`; - const mockApiResponseData = { result: 'Data from API' }; + const mockApiResponseData = {result: 'Data from API'}; beforeEach(() => { - tool = ToolboxTool(mockSession, baseURL, toolName, toolDescription, basicParamSchema); + tool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); }); it('should make a POST request to the correct URL with the validated payload', async () => { - mockAxiosPost.mockResolvedValueOnce({ data: mockApiResponseData } as AxiosResponse); + mockAxiosPost.mockResolvedValueOnce({ + data: mockApiResponseData, + } as AxiosResponse); const result = await tool(validArgs); @@ -194,13 +273,18 @@ describe('ToolboxTool', () => { }); it('should re-throw the error and log to console.error if API call fails with response data', async () => { - const apiErrorResponseData = { error: 'detail from server' }; - const apiError = createApiError('API request failed', apiErrorResponseData); + const apiErrorResponseData = {error: 'detail from server'}; + const apiError = createApiError( + 'API request failed', + apiErrorResponseData + ); mockAxiosPost.mockRejectedValueOnce(apiError); try { await tool(validArgs); - throw new Error('Expected tool call to throw an API error with response data, but it did not.'); + throw new Error( + 'Expected tool call to throw an API error with response data, but it did not.' + ); } catch (e: any) { expect(e).toBe(apiError); } @@ -218,7 +302,9 @@ describe('ToolboxTool', () => { try { await tool(validArgs); - throw new Error('Expected tool call to throw an API error without response data, but it did not.'); + throw new Error( + 'Expected tool call to throw an API error without response data, but it did not.' + ); } catch (e: any) { expect(e).toBe(apiError); } @@ -229,4 +315,4 @@ describe('ToolboxTool', () => { ); }); }); -}); \ No newline at end of file +}); From 0baf9ab66cde2a8b409fa2ed5ee810ab48e419bf Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 18:47:26 +0530 Subject: [PATCH 15/57] move files to different PR --- .../toolbox-core/src/toolbox_core/client.ts | 81 ----- .../toolbox-core/src/toolbox_core/tool.ts | 72 ---- .../toolbox-core/src/toolbox_core/utils.ts | 0 packages/toolbox-core/test/test.client.ts | 264 --------------- packages/toolbox-core/test/test.tool.ts | 318 ------------------ 5 files changed, 735 deletions(-) delete mode 100644 packages/toolbox-core/src/toolbox_core/client.ts delete mode 100644 packages/toolbox-core/src/toolbox_core/tool.ts delete mode 100644 packages/toolbox-core/src/toolbox_core/utils.ts delete mode 100644 packages/toolbox-core/test/test.client.ts delete mode 100644 packages/toolbox-core/test/test.tool.ts diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts deleted file mode 100644 index 69da949..0000000 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2025 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import {ToolboxTool} from './tool'; -import axios from 'axios'; -import type {AxiosInstance, AxiosResponse} from 'axios'; -import { - ZodManifestSchema, - createZodObjectSchemaFromParameters, -} from './protocol'; - -class ToolboxClient { - /** @private */ private _baseUrl: string; - /** @private */ private _session: AxiosInstance; - - /** - * @param {string} url - The base URL for the Toolbox service API. - */ - constructor(url: string, session?: AxiosInstance) { - this._baseUrl = url; - this._session = session || axios.create({baseURL: url}); - } - - /** - * @param {string} toolName - Name of the tool. - * @returns {ToolboxTool} - A ToolboxTool instance. - */ - async loadTool(toolName: string): Promise> { - const url = `${this._baseUrl}/api/tool/${toolName}`; - try { - const response: AxiosResponse = await this._session.get(url); - const responseData = response.data; - - const manifestResponse = ZodManifestSchema.safeParse(responseData); - if (manifestResponse.success) { - const manifest = manifestResponse.data; - if ( - manifest.tools && - Object.prototype.hasOwnProperty.call(manifest.tools, toolName) - ) { - const specificToolSchema = manifest.tools[toolName]; - const paramZodSchema = createZodObjectSchemaFromParameters( - specificToolSchema.parameters - ); - return ToolboxTool( - this._session, - this._baseUrl, - toolName, - specificToolSchema.description, - paramZodSchema - ); - } else { - throw new Error(`Tool "${toolName}" not found in manifest.`); - } - } else { - throw new Error( - `Invalid manifest structure received: ${manifestResponse.error.message}` - ); - } - } catch (error) { - console.error( - `Error fetching data from ${url}:`, - (error as any).response?.data || (error as any).message - ); - throw error; - } - } -} - -export {ToolboxClient}; diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts deleted file mode 100644 index fecd911..0000000 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2025 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import {ZodObject, ZodError} from 'zod'; -import {AxiosInstance, AxiosResponse} from 'axios'; - -function ToolboxTool( - session: AxiosInstance, - baseUrl: string, - name: string, - description: string, - paramSchema: ZodObject -) { - const toolUrl = `${baseUrl}/api/tool/${name}/invoke`; - - const callable = async function (callArguments: Record = {}) { - let validatedPayload: Record; - try { - validatedPayload = paramSchema.parse(callArguments); - } catch (error) { - if (error instanceof ZodError) { - const errorMessages = error.errors.map( - e => `${e.path.join('.') || 'payload'}: ${e.message}` - ); - throw new Error( - `Argument validation failed for tool "${name}":\n - ${errorMessages.join('\n - ')}` - ); - } - throw new Error(`Argument validation failed: ${String(error)}`); - } - - try { - const response: AxiosResponse = await session.post( - toolUrl, - validatedPayload - ); - return response.data; - } catch (error) { - console.error( - `Error posting data to ${toolUrl}:`, - (error as any).response?.data || (error as any).message - ); - throw error; - } - }; - callable.toolName = name; - callable.description = description; - callable.params = paramSchema; - callable.getName = function () { - return this.toolName; - }; - callable.getDescription = function () { - return this.description; - }; - callable.getParamSchema = function () { - return this.params; - }; - return callable; -} - -export {ToolboxTool}; diff --git a/packages/toolbox-core/src/toolbox_core/utils.ts b/packages/toolbox-core/src/toolbox_core/utils.ts deleted file mode 100644 index e69de29..0000000 diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts deleted file mode 100644 index 3120841..0000000 --- a/packages/toolbox-core/test/test.client.ts +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2025 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import {ToolboxClient} from '../src/toolbox_core/client'; -import {ToolboxTool} from '../src/toolbox_core/tool'; -import { - ZodManifestSchema, - createZodObjectSchemaFromParameters, -} from '../src/toolbox_core/protocol'; -import axios, {AxiosInstance, AxiosResponse} from 'axios'; - -// --- Mocking External Dependencies --- -jest.mock('axios'); -const mockedAxios = axios as jest.Mocked; - -jest.mock('../src/toolbox_core/tool', () => ({ - ToolboxTool: jest.fn(), -})); -const MockedToolboxToolFactory = ToolboxTool as jest.MockedFunction< - typeof ToolboxTool ->; - -jest.mock('../src/toolbox_core/protocol', () => ({ - ZodManifestSchema: { - safeParse: jest.fn(), - }, - createZodObjectSchemaFromParameters: jest.fn(), -})); -const MockedZodManifestSchema = ZodManifestSchema as jest.Mocked< - typeof ZodManifestSchema ->; -const MockedCreateZodObjectSchemaFromParameters = - createZodObjectSchemaFromParameters as jest.MockedFunction< - typeof createZodObjectSchemaFromParameters - >; - -// --- Test Helper Functions --- -type ApiErrorWithMessage = Error & {response?: {data: any}}; -const createApiError = ( - message: string, - responseData?: any -): ApiErrorWithMessage => { - const error = new Error(message) as ApiErrorWithMessage; - if (responseData !== undefined) { - error.response = {data: responseData}; - } - return error; -}; - -describe('ToolboxClient', () => { - const testBaseUrl = 'http://api.example.com'; - let consoleErrorSpy: jest.SpyInstance; - let mockSessionGet: jest.Mock; - - beforeEach(() => { - jest.resetAllMocks(); - - mockSessionGet = jest.fn(); - mockedAxios.create.mockReturnValue({ - get: mockSessionGet, - } as unknown as AxiosInstance); - - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - consoleErrorSpy.mockRestore(); - }); - - describe('constructor', () => { - it('should set baseUrl and create a new session if one is not provided', () => { - const client = new ToolboxClient(testBaseUrl); - - expect((client as any)._baseUrl).toBe(testBaseUrl); - expect(mockedAxios.create).toHaveBeenCalledTimes(1); - expect(mockedAxios.create).toHaveBeenCalledWith({baseURL: testBaseUrl}); - expect((client as any)._session.get).toBe(mockSessionGet); - }); - - it('should set baseUrl and use the provided session if one is given', () => { - const customMockSession = { - get: mockSessionGet, - } as unknown as AxiosInstance; - const client = new ToolboxClient(testBaseUrl, customMockSession); - - expect((client as any)._baseUrl).toBe(testBaseUrl); - expect((client as any)._session).toBe(customMockSession); - expect(mockedAxios.create).not.toHaveBeenCalled(); - }); - }); - - describe('loadTool', () => { - const toolName = 'calculator'; - const expectedApiUrl = `${testBaseUrl}/api/tool/${toolName}`; - let client: ToolboxClient; - - beforeEach(() => { - client = new ToolboxClient(testBaseUrl); - }); - - const setupMocksForSuccessfulLoad = ( - toolDefinition: object, - overrides: Partial<{ - manifestData: object; - zodParamsSchema: object; - toolInstance: object; - }> = {} - ) => { - const manifestData = overrides.manifestData || { - serverVersion: '1.0.0', - tools: {[toolName]: toolDefinition}, - }; - const zodParamsSchema = overrides.zodParamsSchema || { - _isMockZodParamSchema: true, - forTool: toolName, - }; - const toolInstance = overrides.toolInstance || { - _isMockTool: true, - loadedName: toolName, - }; - - mockSessionGet.mockResolvedValueOnce({ - data: manifestData, - } as AxiosResponse); - MockedZodManifestSchema.safeParse.mockReturnValueOnce({ - success: true, - data: manifestData, - } as any); - MockedCreateZodObjectSchemaFromParameters.mockReturnValueOnce( - zodParamsSchema as any - ); - MockedToolboxToolFactory.mockReturnValueOnce(toolInstance as any); - - return {manifestData, zodParamsSchema, toolInstance}; - }; - - it('should successfully load a tool with valid manifest and API response', async () => { - const mockToolDefinition = { - description: 'Performs calculations', - parameters: [ - {name: 'expression', type: 'string', description: 'Math expression'}, - ], - }; - - const {zodParamsSchema, toolInstance, manifestData} = - setupMocksForSuccessfulLoad(mockToolDefinition); - const loadedTool = await client.loadTool(toolName); - - expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); - expect(MockedZodManifestSchema.safeParse).toHaveBeenCalledWith( - manifestData - ); - expect(MockedCreateZodObjectSchemaFromParameters).toHaveBeenCalledWith( - mockToolDefinition.parameters - ); - expect(MockedToolboxToolFactory).toHaveBeenCalledWith( - (client as any)._session, - testBaseUrl, - toolName, - mockToolDefinition.description, - zodParamsSchema - ); - expect(loadedTool).toBe(toolInstance); - }); - - it('should throw an error if manifest parsing fails', async () => { - const mockApiResponseData = {invalid: 'manifest structure'}; - const mockZodErrorDetail = {message: 'Zod validation failed on manifest'}; - mockSessionGet.mockResolvedValueOnce({ - data: mockApiResponseData, - } as AxiosResponse); - MockedZodManifestSchema.safeParse.mockReturnValueOnce({ - success: false, - error: mockZodErrorDetail, - } as any); - - await expect(client.loadTool(toolName)).rejects.toThrow( - `Invalid manifest structure received: ${mockZodErrorDetail.message}` - ); - expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); - expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); - }); - - it('should throw an error if manifest.tools key is missing', async () => { - const mockManifestWithoutTools = {serverVersion: '1.0.0'}; // 'tools' key absent - setupMocksForSuccessfulLoad( - {description: '', parameters: []}, - {manifestData: mockManifestWithoutTools} - ); - MockedZodManifestSchema.safeParse.mockReturnValueOnce({ - success: true, - data: mockManifestWithoutTools, - } as any); - - await expect(client.loadTool(toolName)).rejects.toThrow( - `Tool "${toolName}" not found in manifest.` - ); - expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); - expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); - }); - - it('should throw an error if the specific tool is not found in manifest.tools', async () => { - const mockManifestWithOtherTools = { - serverVersion: '1.0.0', - tools: {anotherTool: {description: 'A different tool', parameters: []}}, - }; - mockSessionGet.mockResolvedValueOnce({ - data: mockManifestWithOtherTools, - } as AxiosResponse); - MockedZodManifestSchema.safeParse.mockReturnValueOnce({ - success: true, - data: mockManifestWithOtherTools, - } as any); - - await expect(client.loadTool(toolName)).rejects.toThrow( - `Tool "${toolName}" not found in manifest.` - ); - expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); - expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); - }); - - it('should throw and log error if API GET request fails with response data', async () => { - const errorResponseData = {code: 500, message: 'Server-side issue'}; - const apiError = createApiError( - 'API call failed unexpectedly', - errorResponseData - ); - mockSessionGet.mockRejectedValueOnce(apiError); - - await expect(client.loadTool(toolName)).rejects.toThrow(apiError); - expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `Error fetching data from ${expectedApiUrl}:`, - errorResponseData - ); - expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); - }); - - it('should throw and log error (using error.message) if API GET request fails without response data', async () => { - const errorMessage = 'Network unavailable'; - const apiError = createApiError(errorMessage); - mockSessionGet.mockRejectedValueOnce(apiError); - - await expect(client.loadTool(toolName)).rejects.toThrow(apiError); - expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `Error fetching data from ${expectedApiUrl}:`, - errorMessage - ); - expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/toolbox-core/test/test.tool.ts b/packages/toolbox-core/test/test.tool.ts deleted file mode 100644 index bebba58..0000000 --- a/packages/toolbox-core/test/test.tool.ts +++ /dev/null @@ -1,318 +0,0 @@ -// Copyright 2025 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import {ToolboxTool} from '../src/toolbox_core/tool'; -import {z, ZodObject} from 'zod'; -import {AxiosInstance, AxiosResponse} from 'axios'; - -// Global mocks for Axios -const mockAxiosPost = jest.fn(); -const mockSession = { - post: mockAxiosPost, -} as unknown as AxiosInstance; - -// Helper to create structured API error objects for testing -type ApiErrorWithMessage = Error & {response?: {data: any}}; -const createApiError = ( - message: string, - responseData?: any -): ApiErrorWithMessage => { - const error = new Error(message) as ApiErrorWithMessage; - if (responseData !== undefined) { - error.response = {data: responseData}; - } - return error; -}; - -describe('ToolboxTool', () => { - // Common constants for the tool - const baseURL = 'http://api.example.com'; - const toolName = 'myTestTool'; - const toolDescription = 'This is a description for the test tool.'; - - // Variables to be initialized in beforeEach - let basicParamSchema: ZodObject; - let consoleErrorSpy: jest.SpyInstance; - let tool: ReturnType; - - beforeEach(() => { - // Reset mocks before each test - mockAxiosPost.mockReset(); - - // Initialize a basic schema used by many tests - basicParamSchema = z.object({ - query: z.string().min(1, 'Query cannot be empty'), - limit: z.number().optional(), - }); - - // Spy on console.error to prevent logging and allow assertions - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - // Restore the original console.error - consoleErrorSpy.mockRestore(); - }); - - describe('Factory Properties and Getters', () => { - beforeEach(() => { - tool = ToolboxTool( - mockSession, - baseURL, - toolName, - toolDescription, - basicParamSchema - ); - }); - - it('should correctly assign toolName, description, and params to the callable function', () => { - expect(tool.toolName).toBe(toolName); - expect(tool.description).toBe(toolDescription); - expect(tool.params).toBe(basicParamSchema); - }); - - it('getName() should return the tool name', () => { - expect(tool.getName()).toBe(toolName); - }); - - it('getDescription() should return the tool description', () => { - expect(tool.getDescription()).toBe(toolDescription); - }); - - it('getParamSchema() should return the parameter schema', () => { - expect(tool.getParamSchema()).toBe(basicParamSchema); - }); - }); - - describe('Callable Function - Argument Validation', () => { - it('should call paramSchema.parse with the provided arguments', async () => { - const currentTool = ToolboxTool( - mockSession, - baseURL, - toolName, - toolDescription, - basicParamSchema - ); - const parseSpy = jest.spyOn(basicParamSchema, 'parse'); - const callArgs = {query: 'test query'}; - mockAxiosPost.mockResolvedValueOnce({data: 'success'} as AxiosResponse); - - await currentTool(callArgs); - - expect(parseSpy).toHaveBeenCalledWith(callArgs); - parseSpy.mockRestore(); - }); - - it('should throw a formatted ZodError if argument validation fails', async () => { - const currentTool = ToolboxTool( - mockSession, - baseURL, - toolName, - toolDescription, - basicParamSchema - ); - const invalidArgs = {query: ''}; // Fails because of empty string - - try { - await currentTool(invalidArgs); - throw new Error( - `Expected currentTool to throw a Zod validation error for tool "${toolName}", but it did not.` - ); - } catch (e: any) { - expect(e.message).toBe( - `Argument validation failed for tool "${toolName}":\n - query: Query cannot be empty` - ); - } - expect(mockAxiosPost).not.toHaveBeenCalled(); - }); - - it('should handle multiple ZodError issues in the validation error message', async () => { - const complexSchema = z.object({ - name: z.string().min(1, 'Name is required'), - age: z.number().positive('Age must be positive'), - }); - const currentTool = ToolboxTool( - mockSession, - baseURL, - toolName, - toolDescription, - complexSchema - ); - const invalidArgs = {name: '', age: -5}; - - try { - await currentTool(invalidArgs); - throw new Error( - 'Expected currentTool to throw a Zod validation error, but it did not.' - ); - } catch (e: any) { - expect(e.message).toEqual( - expect.stringContaining( - `Argument validation failed for tool "${toolName}":` - ) - ); - expect(e.message).toEqual( - expect.stringContaining('name: Name is required') - ); - expect(e.message).toEqual( - expect.stringContaining('age: Age must be positive') - ); - } - expect(mockAxiosPost).not.toHaveBeenCalled(); - }); - - it('should throw a generic error if paramSchema.parse throws a non-ZodError', async () => { - const customError = new Error('A non-Zod parsing error occurred!'); - const failingSchema = { - parse: jest.fn().mockImplementation(() => { - throw customError; - }), - } as unknown as ZodObject; - const currentTool = ToolboxTool( - mockSession, - baseURL, - toolName, - toolDescription, - failingSchema - ); - const callArgs = {query: 'some query'}; - - try { - await currentTool(callArgs); - throw new Error( - 'Expected currentTool to throw a non-Zod error during parsing, but it did not.' - ); - } catch (e: any) { - expect(e.message).toBe( - `Argument validation failed: ${String(customError)}` - ); - } - expect(mockAxiosPost).not.toHaveBeenCalled(); - }); - - it('should use an empty object as default if no arguments are provided and schema allows it', async () => { - const emptySchema = z.object({}); - const parseSpy = jest.spyOn(emptySchema, 'parse'); - const currentTool = ToolboxTool( - mockSession, - baseURL, - toolName, - toolDescription, - emptySchema - ); - mockAxiosPost.mockResolvedValueOnce({data: 'success'}); - - await currentTool(); - - expect(parseSpy).toHaveBeenCalledWith({}); - expect(mockAxiosPost).toHaveBeenCalled(); - parseSpy.mockRestore(); - }); - - it('should fail validation if no arguments are given and schema requires them', async () => { - const currentTool = ToolboxTool( - mockSession, - baseURL, - toolName, - toolDescription, - basicParamSchema - ); - try { - await currentTool(); - throw new Error( - `Expected currentTool to throw a Zod validation error for tool "${toolName}" when no args provided, but it did not.` - ); - } catch (e: any) { - expect(e.message).toEqual( - expect.stringContaining( - 'Argument validation failed for tool "myTestTool":' - ) - ); - expect(e.message).toEqual(expect.stringContaining('query: Required')); - } - expect(mockAxiosPost).not.toHaveBeenCalled(); - }); - }); - - describe('Callable Function - API Call Execution', () => { - const validArgs = {query: 'search term', limit: 10}; - const expectedUrl = `${baseURL}/api/tool/${toolName}/invoke`; - const mockApiResponseData = {result: 'Data from API'}; - - beforeEach(() => { - tool = ToolboxTool( - mockSession, - baseURL, - toolName, - toolDescription, - basicParamSchema - ); - }); - - it('should make a POST request to the correct URL with the validated payload', async () => { - mockAxiosPost.mockResolvedValueOnce({ - data: mockApiResponseData, - } as AxiosResponse); - - const result = await tool(validArgs); - - expect(mockAxiosPost).toHaveBeenCalledTimes(1); - expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); - expect(result).toEqual(mockApiResponseData); - }); - - it('should re-throw the error and log to console.error if API call fails with response data', async () => { - const apiErrorResponseData = {error: 'detail from server'}; - const apiError = createApiError( - 'API request failed', - apiErrorResponseData - ); - mockAxiosPost.mockRejectedValueOnce(apiError); - - try { - await tool(validArgs); - throw new Error( - 'Expected tool call to throw an API error with response data, but it did not.' - ); - } catch (e: any) { - expect(e).toBe(apiError); - } - expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `Error posting data to ${expectedUrl}:`, - apiErrorResponseData - ); - }); - - it('should re-throw the error and log (using error.message) if API call fails without response data', async () => { - const apiErrorMessage = 'Network connection refused'; - const apiError = createApiError(apiErrorMessage); // No responseData - mockAxiosPost.mockRejectedValueOnce(apiError); - - try { - await tool(validArgs); - throw new Error( - 'Expected tool call to throw an API error without response data, but it did not.' - ); - } catch (e: any) { - expect(e).toBe(apiError); - } - expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `Error posting data to ${expectedUrl}:`, - apiErrorMessage - ); - }); - }); -}); From 660b41002fe9ffbf9991176f0645328ee7927fa2 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 18:48:24 +0530 Subject: [PATCH 16/57] move files to different pr --- .../toolbox-core/src/toolbox_core/client.ts | 39 +++++++++++++++++++ .../toolbox-core/src/toolbox_core/tool.ts | 33 ++++++++++++++++ packages/toolbox-core/test/test.client.ts | 10 +++++ 3 files changed, 82 insertions(+) create mode 100644 packages/toolbox-core/src/toolbox_core/client.ts create mode 100644 packages/toolbox-core/src/toolbox_core/tool.ts create mode 100644 packages/toolbox-core/test/test.client.ts diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts new file mode 100644 index 0000000..63d5845 --- /dev/null +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -0,0 +1,39 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {ToolboxTool} from './tool'; + +class ToolboxClient { + /** @private */ _baseUrl; + + /** + * @param {string} url - The base URL for the Toolbox service API. + */ + constructor(url: string) { + this._baseUrl = url; + } + + /** + * @param {int} num1 - First number. + * @param {int} num2 - Second number. + * @returns {int} - Mock API response. + */ + async getToolResponse(num1: number, num2: number) { + const tool = ToolboxTool('tool1'); + const response = await tool({a: num1, b: num2}); + return response; + } +} + +export {ToolboxClient}; diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts new file mode 100644 index 0000000..2cc72d6 --- /dev/null +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -0,0 +1,33 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +function ToolboxTool(name: string) { + const callable = async function (action: {a: number; b: number}) { + // MOCK API CALL + async function api_resp(a: number, b: number): Promise { + await new Promise(resolve => setTimeout(resolve, 10)); + return a + 2 * b; + } + + const result = await api_resp(action.a, action.b); + return result; + }; + callable.toolName = name; + callable.getName = function () { + return this.toolName; + }; + return callable; +} + +export {ToolboxTool}; diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts new file mode 100644 index 0000000..0bddb12 --- /dev/null +++ b/packages/toolbox-core/test/test.client.ts @@ -0,0 +1,10 @@ +import {ToolboxClient} from '../src/toolbox_core/client'; + +const client = new ToolboxClient('https://some_base_url'); + +describe('getToolResponse', () => { + test('Should return a specific value based on inputs', async () => { + const response = await client.getToolResponse(3, 4); + expect(response).toBe(11); + }); +}); From cb3a7be9853279c2ba03a184a7e9419d21dae95f Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 18:54:40 +0530 Subject: [PATCH 17/57] move files to correct pr --- .../toolbox-core/src/toolbox_core/client.ts | 62 +++- .../toolbox-core/src/toolbox_core/tool.ts | 57 +++- packages/toolbox-core/test/test.client.ts | 266 ++++++++++++++- packages/toolbox-core/test/test.tool.ts | 318 ++++++++++++++++++ 4 files changed, 678 insertions(+), 25 deletions(-) create mode 100644 packages/toolbox-core/test/test.tool.ts diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index 63d5845..bf86e77 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -13,27 +13,69 @@ // limitations under the License. import {ToolboxTool} from './tool'; +import axios from 'axios'; +import type {AxiosInstance, AxiosResponse} from 'axios'; +import { + ZodManifestSchema, + createZodObjectSchemaFromParameters, +} from './protocol'; class ToolboxClient { - /** @private */ _baseUrl; + /** @private */ private _baseUrl: string; + /** @private */ private _session: AxiosInstance; /** * @param {string} url - The base URL for the Toolbox service API. */ - constructor(url: string) { + constructor(url: string, session?: AxiosInstance) { this._baseUrl = url; + this._session = session || axios.create({baseURL: url}); } /** - * @param {int} num1 - First number. - * @param {int} num2 - Second number. - * @returns {int} - Mock API response. + * @param {string} toolName - Name of the tool. + * @returns {ToolboxTool} - A ToolboxTool instance. */ - async getToolResponse(num1: number, num2: number) { - const tool = ToolboxTool('tool1'); - const response = await tool({a: num1, b: num2}); - return response; + async loadTool(toolName: string): Promise> { + const url = `${this._baseUrl}/api/tool/${toolName}`; + try { + const response: AxiosResponse = await this._session.get(url); + const responseData = response.data; + + const manifestResponse = ZodManifestSchema.safeParse(responseData); + if (manifestResponse.success) { + const manifest = manifestResponse.data; + if ( + manifest.tools && + Object.prototype.hasOwnProperty.call(manifest.tools, toolName) + ) { + const specificToolSchema = manifest.tools[toolName]; + const paramZodSchema = createZodObjectSchemaFromParameters( + specificToolSchema.parameters + ); + return ToolboxTool( + this._session, + this._baseUrl, + toolName, + specificToolSchema.description, + paramZodSchema + ); + } else { + throw new Error(`Tool "${toolName}" not found in manifest.`); + } + } else { + throw new Error( + `Invalid manifest structure received: ${manifestResponse.error.message}` + ); + } + } catch (error) { + console.error( + `Error fetching data from ${url}:`, + (error as any).response?.data || (error as any).message + ); + throw error; + } } } -export {ToolboxClient}; +export {ToolboxClient}; \ No newline at end of file diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index 2cc72d6..b94cdd8 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -12,22 +12,61 @@ // See the License for the specific language governing permissions and // limitations under the License. -function ToolboxTool(name: string) { - const callable = async function (action: {a: number; b: number}) { - // MOCK API CALL - async function api_resp(a: number, b: number): Promise { - await new Promise(resolve => setTimeout(resolve, 10)); - return a + 2 * b; +import {ZodObject, ZodError} from 'zod'; +import {AxiosInstance, AxiosResponse} from 'axios'; + +function ToolboxTool( + session: AxiosInstance, + baseUrl: string, + name: string, + description: string, + paramSchema: ZodObject +) { + const toolUrl = `${baseUrl}/api/tool/${name}/invoke`; + + const callable = async function (callArguments: Record = {}) { + let validatedPayload: Record; + try { + validatedPayload = paramSchema.parse(callArguments); + } catch (error) { + if (error instanceof ZodError) { + const errorMessages = error.errors.map( + e => `${e.path.join('.') || 'payload'}: ${e.message}` + ); + throw new Error( + `Argument validation failed for tool "${name}":\n - ${errorMessages.join('\n - ')}` + ); + } + throw new Error(`Argument validation failed: ${String(error)}`); } - const result = await api_resp(action.a, action.b); - return result; + try { + const response: AxiosResponse = await session.post( + toolUrl, + validatedPayload + ); + return response.data; + } catch (error) { + console.error( + `Error posting data to ${toolUrl}:`, + (error as any).response?.data || (error as any).message + ); + throw error; + } }; callable.toolName = name; + callable.description = description; + callable.params = paramSchema; callable.getName = function () { return this.toolName; }; + callable.getDescription = function () { + return this.description; + }; + callable.getParamSchema = function () { + return this.params; + }; return callable; } -export {ToolboxTool}; +export {ToolboxTool}; \ No newline at end of file diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 0bddb12..0a4a966 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -1,10 +1,264 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + import {ToolboxClient} from '../src/toolbox_core/client'; +import {ToolboxTool} from '../src/toolbox_core/tool'; +import { + ZodManifestSchema, + createZodObjectSchemaFromParameters, +} from '../src/toolbox_core/protocol'; +import axios, {AxiosInstance, AxiosResponse} from 'axios'; + +// --- Mocking External Dependencies --- +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +jest.mock('../src/toolbox_core/tool', () => ({ + ToolboxTool: jest.fn(), +})); +const MockedToolboxToolFactory = ToolboxTool as jest.MockedFunction< + typeof ToolboxTool +>; + +jest.mock('../src/toolbox_core/protocol', () => ({ + ZodManifestSchema: { + safeParse: jest.fn(), + }, + createZodObjectSchemaFromParameters: jest.fn(), +})); +const MockedZodManifestSchema = ZodManifestSchema as jest.Mocked< + typeof ZodManifestSchema +>; +const MockedCreateZodObjectSchemaFromParameters = + createZodObjectSchemaFromParameters as jest.MockedFunction< + typeof createZodObjectSchemaFromParameters + >; + +// --- Test Helper Functions --- +type ApiErrorWithMessage = Error & {response?: {data: any}}; +const createApiError = ( + message: string, + responseData?: any +): ApiErrorWithMessage => { + const error = new Error(message) as ApiErrorWithMessage; + if (responseData !== undefined) { + error.response = {data: responseData}; + } + return error; +}; + +describe('ToolboxClient', () => { + const testBaseUrl = 'http://api.example.com'; + let consoleErrorSpy: jest.SpyInstance; + let mockSessionGet: jest.Mock; + + beforeEach(() => { + jest.resetAllMocks(); + + mockSessionGet = jest.fn(); + mockedAxios.create.mockReturnValue({ + get: mockSessionGet, + } as unknown as AxiosInstance); + + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + describe('constructor', () => { + it('should set baseUrl and create a new session if one is not provided', () => { + const client = new ToolboxClient(testBaseUrl); + + expect((client as any)._baseUrl).toBe(testBaseUrl); + expect(mockedAxios.create).toHaveBeenCalledTimes(1); + expect(mockedAxios.create).toHaveBeenCalledWith({baseURL: testBaseUrl}); + expect((client as any)._session.get).toBe(mockSessionGet); + }); + + it('should set baseUrl and use the provided session if one is given', () => { + const customMockSession = { + get: mockSessionGet, + } as unknown as AxiosInstance; + const client = new ToolboxClient(testBaseUrl, customMockSession); + + expect((client as any)._baseUrl).toBe(testBaseUrl); + expect((client as any)._session).toBe(customMockSession); + expect(mockedAxios.create).not.toHaveBeenCalled(); + }); + }); + + describe('loadTool', () => { + const toolName = 'calculator'; + const expectedApiUrl = `${testBaseUrl}/api/tool/${toolName}`; + let client: ToolboxClient; + + beforeEach(() => { + client = new ToolboxClient(testBaseUrl); + }); + + const setupMocksForSuccessfulLoad = ( + toolDefinition: object, + overrides: Partial<{ + manifestData: object; + zodParamsSchema: object; + toolInstance: object; + }> = {} + ) => { + const manifestData = overrides.manifestData || { + serverVersion: '1.0.0', + tools: {[toolName]: toolDefinition}, + }; + const zodParamsSchema = overrides.zodParamsSchema || { + _isMockZodParamSchema: true, + forTool: toolName, + }; + const toolInstance = overrides.toolInstance || { + _isMockTool: true, + loadedName: toolName, + }; + + mockSessionGet.mockResolvedValueOnce({ + data: manifestData, + } as AxiosResponse); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ + success: true, + data: manifestData, + } as any); + MockedCreateZodObjectSchemaFromParameters.mockReturnValueOnce( + zodParamsSchema as any + ); + MockedToolboxToolFactory.mockReturnValueOnce(toolInstance as any); + + return {manifestData, zodParamsSchema, toolInstance}; + }; + + it('should successfully load a tool with valid manifest and API response', async () => { + const mockToolDefinition = { + description: 'Performs calculations', + parameters: [ + {name: 'expression', type: 'string', description: 'Math expression'}, + ], + }; + + const {zodParamsSchema, toolInstance, manifestData} = + setupMocksForSuccessfulLoad(mockToolDefinition); + const loadedTool = await client.loadTool(toolName); + + expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); + expect(MockedZodManifestSchema.safeParse).toHaveBeenCalledWith( + manifestData + ); + expect(MockedCreateZodObjectSchemaFromParameters).toHaveBeenCalledWith( + mockToolDefinition.parameters + ); + expect(MockedToolboxToolFactory).toHaveBeenCalledWith( + (client as any)._session, + testBaseUrl, + toolName, + mockToolDefinition.description, + zodParamsSchema + ); + expect(loadedTool).toBe(toolInstance); + }); + + it('should throw an error if manifest parsing fails', async () => { + const mockApiResponseData = {invalid: 'manifest structure'}; + const mockZodErrorDetail = {message: 'Zod validation failed on manifest'}; + mockSessionGet.mockResolvedValueOnce({ + data: mockApiResponseData, + } as AxiosResponse); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ + success: false, + error: mockZodErrorDetail, + } as any); + + await expect(client.loadTool(toolName)).rejects.toThrow( + `Invalid manifest structure received: ${mockZodErrorDetail.message}` + ); + expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); + expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); + }); + + it('should throw an error if manifest.tools key is missing', async () => { + const mockManifestWithoutTools = {serverVersion: '1.0.0'}; // 'tools' key absent + setupMocksForSuccessfulLoad( + {description: '', parameters: []}, + {manifestData: mockManifestWithoutTools} + ); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ + success: true, + data: mockManifestWithoutTools, + } as any); + + await expect(client.loadTool(toolName)).rejects.toThrow( + `Tool "${toolName}" not found in manifest.` + ); + expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); + expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); + }); + + it('should throw an error if the specific tool is not found in manifest.tools', async () => { + const mockManifestWithOtherTools = { + serverVersion: '1.0.0', + tools: {anotherTool: {description: 'A different tool', parameters: []}}, + }; + mockSessionGet.mockResolvedValueOnce({ + data: mockManifestWithOtherTools, + } as AxiosResponse); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ + success: true, + data: mockManifestWithOtherTools, + } as any); + + await expect(client.loadTool(toolName)).rejects.toThrow( + `Tool "${toolName}" not found in manifest.` + ); + expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); + expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); + }); + + it('should throw and log error if API GET request fails with response data', async () => { + const errorResponseData = {code: 500, message: 'Server-side issue'}; + const apiError = createApiError( + 'API call failed unexpectedly', + errorResponseData + ); + mockSessionGet.mockRejectedValueOnce(apiError); + + await expect(client.loadTool(toolName)).rejects.toThrow(apiError); + expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error fetching data from ${expectedApiUrl}:`, + errorResponseData + ); + expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); + }); -const client = new ToolboxClient('https://some_base_url'); + it('should throw and log error (using error.message) if API GET request fails without response data', async () => { + const errorMessage = 'Network unavailable'; + const apiError = createApiError(errorMessage); + mockSessionGet.mockRejectedValueOnce(apiError); -describe('getToolResponse', () => { - test('Should return a specific value based on inputs', async () => { - const response = await client.getToolResponse(3, 4); - expect(response).toBe(11); + await expect(client.loadTool(toolName)).rejects.toThrow(apiError); + expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error fetching data from ${expectedApiUrl}:`, + errorMessage + ); + expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); + }); }); -}); +}); \ No newline at end of file diff --git a/packages/toolbox-core/test/test.tool.ts b/packages/toolbox-core/test/test.tool.ts new file mode 100644 index 0000000..87bd5c1 --- /dev/null +++ b/packages/toolbox-core/test/test.tool.ts @@ -0,0 +1,318 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {ToolboxTool} from '../src/toolbox_core/tool'; +import {z, ZodObject} from 'zod'; +import {AxiosInstance, AxiosResponse} from 'axios'; + +// Global mocks for Axios +const mockAxiosPost = jest.fn(); +const mockSession = { + post: mockAxiosPost, +} as unknown as AxiosInstance; + +// Helper to create structured API error objects for testing +type ApiErrorWithMessage = Error & {response?: {data: any}}; +const createApiError = ( + message: string, + responseData?: any +): ApiErrorWithMessage => { + const error = new Error(message) as ApiErrorWithMessage; + if (responseData !== undefined) { + error.response = {data: responseData}; + } + return error; +}; + +describe('ToolboxTool', () => { + // Common constants for the tool + const baseURL = 'http://api.example.com'; + const toolName = 'myTestTool'; + const toolDescription = 'This is a description for the test tool.'; + + // Variables to be initialized in beforeEach + let basicParamSchema: ZodObject; + let consoleErrorSpy: jest.SpyInstance; + let tool: ReturnType; + + beforeEach(() => { + // Reset mocks before each test + mockAxiosPost.mockReset(); + + // Initialize a basic schema used by many tests + basicParamSchema = z.object({ + query: z.string().min(1, 'Query cannot be empty'), + limit: z.number().optional(), + }); + + // Spy on console.error to prevent logging and allow assertions + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + // Restore the original console.error + consoleErrorSpy.mockRestore(); + }); + + describe('Factory Properties and Getters', () => { + beforeEach(() => { + tool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); + }); + + it('should correctly assign toolName, description, and params to the callable function', () => { + expect(tool.toolName).toBe(toolName); + expect(tool.description).toBe(toolDescription); + expect(tool.params).toBe(basicParamSchema); + }); + + it('getName() should return the tool name', () => { + expect(tool.getName()).toBe(toolName); + }); + + it('getDescription() should return the tool description', () => { + expect(tool.getDescription()).toBe(toolDescription); + }); + + it('getParamSchema() should return the parameter schema', () => { + expect(tool.getParamSchema()).toBe(basicParamSchema); + }); + }); + + describe('Callable Function - Argument Validation', () => { + it('should call paramSchema.parse with the provided arguments', async () => { + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); + const parseSpy = jest.spyOn(basicParamSchema, 'parse'); + const callArgs = {query: 'test query'}; + mockAxiosPost.mockResolvedValueOnce({data: 'success'} as AxiosResponse); + + await currentTool(callArgs); + + expect(parseSpy).toHaveBeenCalledWith(callArgs); + parseSpy.mockRestore(); + }); + + it('should throw a formatted ZodError if argument validation fails', async () => { + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); + const invalidArgs = {query: ''}; // Fails because of empty string + + try { + await currentTool(invalidArgs); + throw new Error( + `Expected currentTool to throw a Zod validation error for tool "${toolName}", but it did not.` + ); + } catch (e: any) { + expect(e.message).toBe( + `Argument validation failed for tool "${toolName}":\n - query: Query cannot be empty` + ); + } + expect(mockAxiosPost).not.toHaveBeenCalled(); + }); + + it('should handle multiple ZodError issues in the validation error message', async () => { + const complexSchema = z.object({ + name: z.string().min(1, 'Name is required'), + age: z.number().positive('Age must be positive'), + }); + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + complexSchema + ); + const invalidArgs = {name: '', age: -5}; + + try { + await currentTool(invalidArgs); + throw new Error( + 'Expected currentTool to throw a Zod validation error, but it did not.' + ); + } catch (e: any) { + expect(e.message).toEqual( + expect.stringContaining( + `Argument validation failed for tool "${toolName}":` + ) + ); + expect(e.message).toEqual( + expect.stringContaining('name: Name is required') + ); + expect(e.message).toEqual( + expect.stringContaining('age: Age must be positive') + ); + } + expect(mockAxiosPost).not.toHaveBeenCalled(); + }); + + it('should throw a generic error if paramSchema.parse throws a non-ZodError', async () => { + const customError = new Error('A non-Zod parsing error occurred!'); + const failingSchema = { + parse: jest.fn().mockImplementation(() => { + throw customError; + }), + } as unknown as ZodObject; + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + failingSchema + ); + const callArgs = {query: 'some query'}; + + try { + await currentTool(callArgs); + throw new Error( + 'Expected currentTool to throw a non-Zod error during parsing, but it did not.' + ); + } catch (e: any) { + expect(e.message).toBe( + `Argument validation failed: ${String(customError)}` + ); + } + expect(mockAxiosPost).not.toHaveBeenCalled(); + }); + + it('should use an empty object as default if no arguments are provided and schema allows it', async () => { + const emptySchema = z.object({}); + const parseSpy = jest.spyOn(emptySchema, 'parse'); + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + emptySchema + ); + mockAxiosPost.mockResolvedValueOnce({data: 'success'}); + + await currentTool(); + + expect(parseSpy).toHaveBeenCalledWith({}); + expect(mockAxiosPost).toHaveBeenCalled(); + parseSpy.mockRestore(); + }); + + it('should fail validation if no arguments are given and schema requires them', async () => { + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); + try { + await currentTool(); + throw new Error( + `Expected currentTool to throw a Zod validation error for tool "${toolName}" when no args provided, but it did not.` + ); + } catch (e: any) { + expect(e.message).toEqual( + expect.stringContaining( + 'Argument validation failed for tool "myTestTool":' + ) + ); + expect(e.message).toEqual(expect.stringContaining('query: Required')); + } + expect(mockAxiosPost).not.toHaveBeenCalled(); + }); + }); + + describe('Callable Function - API Call Execution', () => { + const validArgs = {query: 'search term', limit: 10}; + const expectedUrl = `${baseURL}/api/tool/${toolName}/invoke`; + const mockApiResponseData = {result: 'Data from API'}; + + beforeEach(() => { + tool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); + }); + + it('should make a POST request to the correct URL with the validated payload', async () => { + mockAxiosPost.mockResolvedValueOnce({ + data: mockApiResponseData, + } as AxiosResponse); + + const result = await tool(validArgs); + + expect(mockAxiosPost).toHaveBeenCalledTimes(1); + expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); + expect(result).toEqual(mockApiResponseData); + }); + + it('should re-throw the error and log to console.error if API call fails with response data', async () => { + const apiErrorResponseData = {error: 'detail from server'}; + const apiError = createApiError( + 'API request failed', + apiErrorResponseData + ); + mockAxiosPost.mockRejectedValueOnce(apiError); + + try { + await tool(validArgs); + throw new Error( + 'Expected tool call to throw an API error with response data, but it did not.' + ); + } catch (e: any) { + expect(e).toBe(apiError); + } + expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error posting data to ${expectedUrl}:`, + apiErrorResponseData + ); + }); + + it('should re-throw the error and log (using error.message) if API call fails without response data', async () => { + const apiErrorMessage = 'Network connection refused'; + const apiError = createApiError(apiErrorMessage); // No responseData + mockAxiosPost.mockRejectedValueOnce(apiError); + + try { + await tool(validArgs); + throw new Error( + 'Expected tool call to throw an API error without response data, but it did not.' + ); + } catch (e: any) { + expect(e).toBe(apiError); + } + expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error posting data to ${expectedUrl}:`, + apiErrorMessage + ); + }); + }); +}); \ No newline at end of file From e6e6d41a758025466c612e59a5be80fa200dfae9 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 18:55:46 +0530 Subject: [PATCH 18/57] lint --- packages/toolbox-core/src/toolbox_core/tool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index b94cdd8..fecd911 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -69,4 +69,4 @@ function ToolboxTool( return callable; } -export {ToolboxTool}; \ No newline at end of file +export {ToolboxTool}; From 662c650d6a8915a16b741af066e4c2294f1858e2 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 18:55:57 +0530 Subject: [PATCH 19/57] lint --- packages/toolbox-core/src/toolbox_core/client.ts | 2 +- packages/toolbox-core/test/test.client.ts | 2 +- packages/toolbox-core/test/test.tool.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index bf86e77..69da949 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -78,4 +78,4 @@ class ToolboxClient { } } -export {ToolboxClient}; \ No newline at end of file +export {ToolboxClient}; diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 0a4a966..3120841 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -261,4 +261,4 @@ describe('ToolboxClient', () => { expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); }); }); -}); \ No newline at end of file +}); diff --git a/packages/toolbox-core/test/test.tool.ts b/packages/toolbox-core/test/test.tool.ts index 87bd5c1..bebba58 100644 --- a/packages/toolbox-core/test/test.tool.ts +++ b/packages/toolbox-core/test/test.tool.ts @@ -315,4 +315,4 @@ describe('ToolboxTool', () => { ); }); }); -}); \ No newline at end of file +}); From d56f9d53120f1506c23366de179f718feb139794 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 19:52:46 +0530 Subject: [PATCH 20/57] lint --- .../toolbox-core/src/toolbox_core/protocol.ts | 6 ++---- packages/toolbox-core/test/test.protocol.ts | 15 ++++++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/protocol.ts b/packages/toolbox-core/src/toolbox_core/protocol.ts index 89b83b5..1afc2db 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.ts +++ b/packages/toolbox-core/src/toolbox_core/protocol.ts @@ -43,7 +43,7 @@ interface ArrayParameter extends BaseParameter { items: ParameterSchema; // Recursive reference to the ParameterSchema type } -type ParameterSchema = +export type ParameterSchema = | StringParameter | IntegerParameter | FloatParameter @@ -114,9 +114,7 @@ function buildZodShapeFromParameter(param: ParameterSchema): ZodTypeAny { default: { // This ensures exhaustiveness at compile time if ParameterSchema is a discriminated union const _exhaustiveCheck: never = param; - throw new Error( - `Unknown parameter type: ${(_exhaustiveCheck as any).type}` - ); + throw new Error(`Unknown parameter type: ${_exhaustiveCheck['type']}`); } } } diff --git a/packages/toolbox-core/test/test.protocol.ts b/packages/toolbox-core/test/test.protocol.ts index 6068ad4..4a80be9 100644 --- a/packages/toolbox-core/test/test.protocol.ts +++ b/packages/toolbox-core/test/test.protocol.ts @@ -17,6 +17,7 @@ import { ZodParameterSchema, ZodToolSchema, ZodManifestSchema, + ParameterSchema, createZodObjectSchemaFromParameters, } from '../src/toolbox_core/protocol'; @@ -273,7 +274,7 @@ describe('ZodManifestSchema', () => { describe('createZodObjectSchemaFromParameters', () => { it('should create an empty Zod object for an empty parameters array (and be strict)', () => { - const params: any[] = []; + const params: ParameterSchema[] = []; const schema = createZodObjectSchemaFromParameters(params); expectParseSuccess(schema, {}); @@ -285,7 +286,7 @@ describe('createZodObjectSchemaFromParameters', () => { }); it('should create a Zod object schema from mixed parameter types and validate data', () => { - const params: any[] = [ + const params: ParameterSchema[] = [ { name: 'username', description: 'User login name', @@ -310,7 +311,7 @@ describe('createZodObjectSchemaFromParameters', () => { }); it('should create a Zod object schema with an array parameter', () => { - const params: any[] = [ + const params: ParameterSchema[] = [ { name: 'tags', description: 'List of tags', @@ -333,7 +334,7 @@ describe('createZodObjectSchemaFromParameters', () => { }); it('should create a Zod object schema with a nested array parameter', () => { - const params: any[] = [ + const params: ParameterSchema[] = [ { name: 'matrix', description: 'A matrix of numbers', @@ -376,12 +377,12 @@ describe('createZodObjectSchemaFromParameters', () => { }); it('should throw an error when creating schema from parameter with unknown type', () => { - const paramsWithUnknownType: any[] = [ + const paramsWithUnknownType: ParameterSchema[] = [ { name: 'faultyParam', description: 'This param has an unhandled type', - type: 'someUnrecognizedType' as any, // Forcing an invalid type - }, + type: 'someUnrecognizedType', + } as unknown as ParameterSchema, ]; expect(() => createZodObjectSchemaFromParameters(paramsWithUnknownType) From 49664dd461460fb638a704eef489a2c2023f0f67 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 20:02:05 +0530 Subject: [PATCH 21/57] lint --- packages/toolbox-core/test/test.client.ts | 46 ++++++++++++++++------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 3120841..4323577 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -18,7 +18,9 @@ import { ZodManifestSchema, createZodObjectSchemaFromParameters, } from '../src/toolbox_core/protocol'; -import axios, {AxiosInstance, AxiosResponse} from 'axios'; +import axios from 'axios'; +import type {AxiosInstance, AxiosResponse} from 'axios'; +import type {ZodObject, ZodRawShape} from 'zod'; // --- Mocking External Dependencies --- jest.mock('axios'); @@ -46,10 +48,10 @@ const MockedCreateZodObjectSchemaFromParameters = >; // --- Test Helper Functions --- -type ApiErrorWithMessage = Error & {response?: {data: any}}; +type ApiErrorWithMessage = Error & {response?: {data: unknown}}; const createApiError = ( message: string, - responseData?: any + responseData?: unknown ): ApiErrorWithMessage => { const error = new Error(message) as ApiErrorWithMessage; if (responseData !== undefined) { @@ -58,6 +60,16 @@ const createApiError = ( return error; }; +// Helper interfaces for Zod safeParse mock return values +interface MockSafeParseSuccess { + success: true; + data: T; +} +interface MockSafeParseError { + success: false; + error: {message: string}; // Simplified for test cases +} + describe('ToolboxClient', () => { const testBaseUrl = 'http://api.example.com'; let consoleErrorSpy: jest.SpyInstance; @@ -82,10 +94,12 @@ describe('ToolboxClient', () => { it('should set baseUrl and create a new session if one is not provided', () => { const client = new ToolboxClient(testBaseUrl); - expect((client as any)._baseUrl).toBe(testBaseUrl); + expect((client as {_baseUrl: string})._baseUrl).toBe(testBaseUrl); expect(mockedAxios.create).toHaveBeenCalledTimes(1); expect(mockedAxios.create).toHaveBeenCalledWith({baseURL: testBaseUrl}); - expect((client as any)._session.get).toBe(mockSessionGet); + expect((client as {_session: AxiosInstance})._session.get).toBe( + mockSessionGet + ); }); it('should set baseUrl and use the provided session if one is given', () => { @@ -94,8 +108,10 @@ describe('ToolboxClient', () => { } as unknown as AxiosInstance; const client = new ToolboxClient(testBaseUrl, customMockSession); - expect((client as any)._baseUrl).toBe(testBaseUrl); - expect((client as any)._session).toBe(customMockSession); + expect((client as {_baseUrl: string})._baseUrl).toBe(testBaseUrl); + expect((client as {_session: AxiosInstance})._session).toBe( + customMockSession + ); expect(mockedAxios.create).not.toHaveBeenCalled(); }); }); @@ -136,11 +152,13 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: manifestData, - } as any); + } as MockSafeParseSuccess); MockedCreateZodObjectSchemaFromParameters.mockReturnValueOnce( - zodParamsSchema as any + zodParamsSchema as ZodObject + ); + MockedToolboxToolFactory.mockReturnValueOnce( + toolInstance as ReturnType ); - MockedToolboxToolFactory.mockReturnValueOnce(toolInstance as any); return {manifestData, zodParamsSchema, toolInstance}; }; @@ -165,7 +183,7 @@ describe('ToolboxClient', () => { mockToolDefinition.parameters ); expect(MockedToolboxToolFactory).toHaveBeenCalledWith( - (client as any)._session, + (client as {_session: AxiosInstance})._session, testBaseUrl, toolName, mockToolDefinition.description, @@ -183,7 +201,7 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: false, error: mockZodErrorDetail, - } as any); + } as MockSafeParseError); await expect(client.loadTool(toolName)).rejects.toThrow( `Invalid manifest structure received: ${mockZodErrorDetail.message}` @@ -201,7 +219,7 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: mockManifestWithoutTools, - } as any); + } as MockSafeParseSuccess); await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` @@ -221,7 +239,7 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: mockManifestWithOtherTools, - } as any); + } as MockSafeParseSuccess); await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` From 56a24a7ff8c01b0fb4da5ee6d9464453982fc8bc Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 20:03:48 +0530 Subject: [PATCH 22/57] lint --- packages/toolbox-core/test/test.tool.ts | 42 +++++++++++++------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/toolbox-core/test/test.tool.ts b/packages/toolbox-core/test/test.tool.ts index bebba58..a119af7 100644 --- a/packages/toolbox-core/test/test.tool.ts +++ b/packages/toolbox-core/test/test.tool.ts @@ -13,7 +13,7 @@ // limitations under the License. import {ToolboxTool} from '../src/toolbox_core/tool'; -import {z, ZodObject} from 'zod'; +import {z, ZodObject, ZodRawShape} from 'zod'; import {AxiosInstance, AxiosResponse} from 'axios'; // Global mocks for Axios @@ -23,10 +23,10 @@ const mockSession = { } as unknown as AxiosInstance; // Helper to create structured API error objects for testing -type ApiErrorWithMessage = Error & {response?: {data: any}}; +type ApiErrorWithMessage = Error & {response?: {data: unknown}}; const createApiError = ( message: string, - responseData?: any + responseData?: unknown ): ApiErrorWithMessage => { const error = new Error(message) as ApiErrorWithMessage; if (responseData !== undefined) { @@ -42,7 +42,7 @@ describe('ToolboxTool', () => { const toolDescription = 'This is a description for the test tool.'; // Variables to be initialized in beforeEach - let basicParamSchema: ZodObject; + let basicParamSchema: ZodObject; let consoleErrorSpy: jest.SpyInstance; let tool: ReturnType; @@ -129,8 +129,8 @@ describe('ToolboxTool', () => { throw new Error( `Expected currentTool to throw a Zod validation error for tool "${toolName}", but it did not.` ); - } catch (e: any) { - expect(e.message).toBe( + } catch (e) { + expect((e as Error).message).toBe( `Argument validation failed for tool "${toolName}":\n - query: Query cannot be empty` ); } @@ -156,16 +156,16 @@ describe('ToolboxTool', () => { throw new Error( 'Expected currentTool to throw a Zod validation error, but it did not.' ); - } catch (e: any) { - expect(e.message).toEqual( + } catch (e) { + expect((e as Error).message).toEqual( expect.stringContaining( `Argument validation failed for tool "${toolName}":` ) ); - expect(e.message).toEqual( + expect((e as Error).message).toEqual( expect.stringContaining('name: Name is required') ); - expect(e.message).toEqual( + expect((e as Error).message).toEqual( expect.stringContaining('age: Age must be positive') ); } @@ -178,7 +178,7 @@ describe('ToolboxTool', () => { parse: jest.fn().mockImplementation(() => { throw customError; }), - } as unknown as ZodObject; + } as unknown as ZodObject; const currentTool = ToolboxTool( mockSession, baseURL, @@ -193,8 +193,8 @@ describe('ToolboxTool', () => { throw new Error( 'Expected currentTool to throw a non-Zod error during parsing, but it did not.' ); - } catch (e: any) { - expect(e.message).toBe( + } catch (e) { + expect((e as Error).message).toBe( `Argument validation failed: ${String(customError)}` ); } @@ -233,13 +233,15 @@ describe('ToolboxTool', () => { throw new Error( `Expected currentTool to throw a Zod validation error for tool "${toolName}" when no args provided, but it did not.` ); - } catch (e: any) { - expect(e.message).toEqual( + } catch (e) { + expect((e as Error).message).toEqual( expect.stringContaining( 'Argument validation failed for tool "myTestTool":' ) ); - expect(e.message).toEqual(expect.stringContaining('query: Required')); + expect((e as Error).message).toEqual( + expect.stringContaining('query: Required') + ); } expect(mockAxiosPost).not.toHaveBeenCalled(); }); @@ -285,8 +287,8 @@ describe('ToolboxTool', () => { throw new Error( 'Expected tool call to throw an API error with response data, but it did not.' ); - } catch (e: any) { - expect(e).toBe(apiError); + } catch (e) { + expect(e as ApiErrorWithMessage).toBe(apiError); } expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); expect(consoleErrorSpy).toHaveBeenCalledWith( @@ -305,8 +307,8 @@ describe('ToolboxTool', () => { throw new Error( 'Expected tool call to throw an API error without response data, but it did not.' ); - } catch (e: any) { - expect(e).toBe(apiError); + } catch (e) { + expect(e as ApiErrorWithMessage).toBe(apiError); } expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); expect(consoleErrorSpy).toHaveBeenCalledWith( From 8ed8314f67a05ef069abaae1733468c29c830a98 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 20:25:02 +0530 Subject: [PATCH 23/57] fix test file --- packages/toolbox-core/test/test.client.ts | 48 +++++++---------------- 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 4323577..0a4a966 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -18,9 +18,7 @@ import { ZodManifestSchema, createZodObjectSchemaFromParameters, } from '../src/toolbox_core/protocol'; -import axios from 'axios'; -import type {AxiosInstance, AxiosResponse} from 'axios'; -import type {ZodObject, ZodRawShape} from 'zod'; +import axios, {AxiosInstance, AxiosResponse} from 'axios'; // --- Mocking External Dependencies --- jest.mock('axios'); @@ -48,10 +46,10 @@ const MockedCreateZodObjectSchemaFromParameters = >; // --- Test Helper Functions --- -type ApiErrorWithMessage = Error & {response?: {data: unknown}}; +type ApiErrorWithMessage = Error & {response?: {data: any}}; const createApiError = ( message: string, - responseData?: unknown + responseData?: any ): ApiErrorWithMessage => { const error = new Error(message) as ApiErrorWithMessage; if (responseData !== undefined) { @@ -60,16 +58,6 @@ const createApiError = ( return error; }; -// Helper interfaces for Zod safeParse mock return values -interface MockSafeParseSuccess { - success: true; - data: T; -} -interface MockSafeParseError { - success: false; - error: {message: string}; // Simplified for test cases -} - describe('ToolboxClient', () => { const testBaseUrl = 'http://api.example.com'; let consoleErrorSpy: jest.SpyInstance; @@ -94,12 +82,10 @@ describe('ToolboxClient', () => { it('should set baseUrl and create a new session if one is not provided', () => { const client = new ToolboxClient(testBaseUrl); - expect((client as {_baseUrl: string})._baseUrl).toBe(testBaseUrl); + expect((client as any)._baseUrl).toBe(testBaseUrl); expect(mockedAxios.create).toHaveBeenCalledTimes(1); expect(mockedAxios.create).toHaveBeenCalledWith({baseURL: testBaseUrl}); - expect((client as {_session: AxiosInstance})._session.get).toBe( - mockSessionGet - ); + expect((client as any)._session.get).toBe(mockSessionGet); }); it('should set baseUrl and use the provided session if one is given', () => { @@ -108,10 +94,8 @@ describe('ToolboxClient', () => { } as unknown as AxiosInstance; const client = new ToolboxClient(testBaseUrl, customMockSession); - expect((client as {_baseUrl: string})._baseUrl).toBe(testBaseUrl); - expect((client as {_session: AxiosInstance})._session).toBe( - customMockSession - ); + expect((client as any)._baseUrl).toBe(testBaseUrl); + expect((client as any)._session).toBe(customMockSession); expect(mockedAxios.create).not.toHaveBeenCalled(); }); }); @@ -152,13 +136,11 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: manifestData, - } as MockSafeParseSuccess); + } as any); MockedCreateZodObjectSchemaFromParameters.mockReturnValueOnce( - zodParamsSchema as ZodObject - ); - MockedToolboxToolFactory.mockReturnValueOnce( - toolInstance as ReturnType + zodParamsSchema as any ); + MockedToolboxToolFactory.mockReturnValueOnce(toolInstance as any); return {manifestData, zodParamsSchema, toolInstance}; }; @@ -183,7 +165,7 @@ describe('ToolboxClient', () => { mockToolDefinition.parameters ); expect(MockedToolboxToolFactory).toHaveBeenCalledWith( - (client as {_session: AxiosInstance})._session, + (client as any)._session, testBaseUrl, toolName, mockToolDefinition.description, @@ -201,7 +183,7 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: false, error: mockZodErrorDetail, - } as MockSafeParseError); + } as any); await expect(client.loadTool(toolName)).rejects.toThrow( `Invalid manifest structure received: ${mockZodErrorDetail.message}` @@ -219,7 +201,7 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: mockManifestWithoutTools, - } as MockSafeParseSuccess); + } as any); await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` @@ -239,7 +221,7 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: mockManifestWithOtherTools, - } as MockSafeParseSuccess); + } as any); await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` @@ -279,4 +261,4 @@ describe('ToolboxClient', () => { expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); }); }); -}); +}); \ No newline at end of file From ad09411f5f74645b8fe9c57c627819cd7cb89f84 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 20:26:11 +0530 Subject: [PATCH 24/57] lint --- packages/toolbox-core/test/test.client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 0a4a966..3120841 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -261,4 +261,4 @@ describe('ToolboxClient', () => { expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); }); }); -}); \ No newline at end of file +}); From 6eb882afee1c04f797f076b88ef8ab381ca57f22 Mon Sep 17 00:00:00 2001 From: Twisha Bansal <58483338+twishabansal@users.noreply.github.com> Date: Mon, 19 May 2025 21:18:45 +0530 Subject: [PATCH 25/57] Update protocol.ts --- packages/toolbox-core/src/toolbox_core/protocol.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/protocol.ts b/packages/toolbox-core/src/toolbox_core/protocol.ts index 1afc2db..05746f6 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.ts +++ b/packages/toolbox-core/src/toolbox_core/protocol.ts @@ -58,7 +58,7 @@ const ZodBaseParameter = z.object({ authSources: z.array(z.string()).optional(), }); -export const ZodParameterSchema: z.ZodType = z.lazy(() => +export const ZodParameterSchema = z.lazy(() => z.discriminatedUnion('type', [ ZodBaseParameter.extend({ type: z.literal('string'), @@ -77,7 +77,7 @@ export const ZodParameterSchema: z.ZodType = z.lazy(() => items: ZodParameterSchema, // Recursive reference for the item's definition }), ]) -); +) as z.ZodType; export const ZodToolSchema = z.object({ description: z.string().min(1, 'Tool description cannot be empty'), From c7e37b972e01b8ea7b5682fa50b3d7a052cd39b1 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 20 May 2025 17:42:03 +0530 Subject: [PATCH 26/57] fix docstrings --- .../toolbox-core/src/toolbox_core/client.ts | 21 ++++++++++++++++--- .../toolbox-core/src/toolbox_core/tool.ts | 15 +++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index 69da949..bdbee6b 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -20,12 +20,20 @@ import { createZodObjectSchemaFromParameters, } from './protocol'; +/** + * An asynchronous client for interacting with a Toolbox service. + * Provides methods to discover and load tools defined by a remote Toolbox + * service endpoint. It manages an underlying client session. + */ class ToolboxClient { /** @private */ private _baseUrl: string; /** @private */ private _session: AxiosInstance; /** - * @param {string} url - The base URL for the Toolbox service API. + * Initializes the ToolboxClient. + * @param {string} url - The base URL for the Toolbox service API (e.g., "http://localhost:5000"). + * @param {AxiosInstance} [session] - Optional Axios instance for making HTTP + * requests. If not provided, a new one will be created. */ constructor(url: string, session?: AxiosInstance) { this._baseUrl = url; @@ -33,8 +41,15 @@ class ToolboxClient { } /** - * @param {string} toolName - Name of the tool. - * @returns {ToolboxTool} - A ToolboxTool instance. + * Asynchronously loads a tool from the server. + * Retrieves the schema for the specified tool from the Toolbox server and * returns a callable (`ToolboxTool`) that can be used to invoke the + * tool remotely. + * + * @param {string} toolName - The unique name or identifier of the tool to load. + * @returns {Promise>} A promise that resolves + * to a ToolboxTool instance, ready for execution. + * @throws {Error} If the tool is not found in the manifest, the manifest structure is invalid, + * or if there's an error fetching data from the API. */ async loadTool(toolName: string): Promise> { const url = `${this._baseUrl}/api/tool/${toolName}`; diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index fecd911..66b12b7 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -15,6 +15,21 @@ import {ZodObject, ZodError} from 'zod'; import {AxiosInstance, AxiosResponse} from 'axios'; +/** + * Creates a callable tool function representing a specific tool on a remote + * Toolbox server. + * + * @param {AxiosInstance} session - The Axios session for making HTTP requests. + * @param {string} baseUrl - The base URL of the Toolbox Server API. + * @param {string} name - The name of the remote tool. + * @param {string} description - A description of the remote tool. + * @param {ZodObject} paramSchema - The Zod schema for validating the tool's parameters. + * @returns {CallableTool & CallableToolProperties} An async function that, when + * called, invokes the tool with the provided arguments. Validates arguments + * against the tool's signature, then sends them + * as a JSON payload in a POST request to the tool's invoke URL. + */ + function ToolboxTool( session: AxiosInstance, baseUrl: string, From a37dec1bdf6435bbc2a0771daa077cc4e160ff00 Mon Sep 17 00:00:00 2001 From: Twisha Bansal <58483338+twishabansal@users.noreply.github.com> Date: Tue, 20 May 2025 19:52:48 +0530 Subject: [PATCH 27/57] Update client.ts --- packages/toolbox-core/src/toolbox_core/client.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index bdbee6b..2606642 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -45,14 +45,14 @@ class ToolboxClient { * Retrieves the schema for the specified tool from the Toolbox server and * returns a callable (`ToolboxTool`) that can be used to invoke the * tool remotely. * - * @param {string} toolName - The unique name or identifier of the tool to load. + * @param {string} name - The unique name or identifier of the tool to load. * @returns {Promise>} A promise that resolves - * to a ToolboxTool instance, ready for execution. + * to a ToolboxTool function, ready for execution. * @throws {Error} If the tool is not found in the manifest, the manifest structure is invalid, * or if there's an error fetching data from the API. */ - async loadTool(toolName: string): Promise> { - const url = `${this._baseUrl}/api/tool/${toolName}`; + async loadTool(name: string): Promise> { + const url = `${this._baseUrl}/api/tool/${name}`; try { const response: AxiosResponse = await this._session.get(url); const responseData = response.data; @@ -62,21 +62,21 @@ class ToolboxClient { const manifest = manifestResponse.data; if ( manifest.tools && - Object.prototype.hasOwnProperty.call(manifest.tools, toolName) + Object.prototype.hasOwnProperty.call(manifest.tools, name) ) { - const specificToolSchema = manifest.tools[toolName]; + const specificToolSchema = manifest.tools[name]; const paramZodSchema = createZodObjectSchemaFromParameters( specificToolSchema.parameters ); return ToolboxTool( this._session, this._baseUrl, - toolName, + name, specificToolSchema.description, paramZodSchema ); } else { - throw new Error(`Tool "${toolName}" not found in manifest.`); + throw new Error(`Tool "${name}" not found in manifest.`); } } else { throw new Error( From 72cdb033d233fe8c870149511b72463afc6814ca Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 18:54:40 +0530 Subject: [PATCH 28/57] move files to correct pr --- .../toolbox-core/src/toolbox_core/client.ts | 62 +++- .../toolbox-core/src/toolbox_core/tool.ts | 57 +++- packages/toolbox-core/test/test.client.ts | 252 +++++++++++++- packages/toolbox-core/test/test.tool.ts | 318 ++++++++++++++++++ 4 files changed, 664 insertions(+), 25 deletions(-) create mode 100644 packages/toolbox-core/test/test.tool.ts diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index 63d5845..bf86e77 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -13,27 +13,69 @@ // limitations under the License. import {ToolboxTool} from './tool'; +import axios from 'axios'; +import type {AxiosInstance, AxiosResponse} from 'axios'; +import { + ZodManifestSchema, + createZodObjectSchemaFromParameters, +} from './protocol'; class ToolboxClient { - /** @private */ _baseUrl; + /** @private */ private _baseUrl: string; + /** @private */ private _session: AxiosInstance; /** * @param {string} url - The base URL for the Toolbox service API. */ - constructor(url: string) { + constructor(url: string, session?: AxiosInstance) { this._baseUrl = url; + this._session = session || axios.create({baseURL: url}); } /** - * @param {int} num1 - First number. - * @param {int} num2 - Second number. - * @returns {int} - Mock API response. + * @param {string} toolName - Name of the tool. + * @returns {ToolboxTool} - A ToolboxTool instance. */ - async getToolResponse(num1: number, num2: number) { - const tool = ToolboxTool('tool1'); - const response = await tool({a: num1, b: num2}); - return response; + async loadTool(toolName: string): Promise> { + const url = `${this._baseUrl}/api/tool/${toolName}`; + try { + const response: AxiosResponse = await this._session.get(url); + const responseData = response.data; + + const manifestResponse = ZodManifestSchema.safeParse(responseData); + if (manifestResponse.success) { + const manifest = manifestResponse.data; + if ( + manifest.tools && + Object.prototype.hasOwnProperty.call(manifest.tools, toolName) + ) { + const specificToolSchema = manifest.tools[toolName]; + const paramZodSchema = createZodObjectSchemaFromParameters( + specificToolSchema.parameters + ); + return ToolboxTool( + this._session, + this._baseUrl, + toolName, + specificToolSchema.description, + paramZodSchema + ); + } else { + throw new Error(`Tool "${toolName}" not found in manifest.`); + } + } else { + throw new Error( + `Invalid manifest structure received: ${manifestResponse.error.message}` + ); + } + } catch (error) { + console.error( + `Error fetching data from ${url}:`, + (error as any).response?.data || (error as any).message + ); + throw error; + } } } -export {ToolboxClient}; +export {ToolboxClient}; \ No newline at end of file diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index 2cc72d6..b94cdd8 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -12,22 +12,61 @@ // See the License for the specific language governing permissions and // limitations under the License. -function ToolboxTool(name: string) { - const callable = async function (action: {a: number; b: number}) { - // MOCK API CALL - async function api_resp(a: number, b: number): Promise { - await new Promise(resolve => setTimeout(resolve, 10)); - return a + 2 * b; +import {ZodObject, ZodError} from 'zod'; +import {AxiosInstance, AxiosResponse} from 'axios'; + +function ToolboxTool( + session: AxiosInstance, + baseUrl: string, + name: string, + description: string, + paramSchema: ZodObject +) { + const toolUrl = `${baseUrl}/api/tool/${name}/invoke`; + + const callable = async function (callArguments: Record = {}) { + let validatedPayload: Record; + try { + validatedPayload = paramSchema.parse(callArguments); + } catch (error) { + if (error instanceof ZodError) { + const errorMessages = error.errors.map( + e => `${e.path.join('.') || 'payload'}: ${e.message}` + ); + throw new Error( + `Argument validation failed for tool "${name}":\n - ${errorMessages.join('\n - ')}` + ); + } + throw new Error(`Argument validation failed: ${String(error)}`); } - const result = await api_resp(action.a, action.b); - return result; + try { + const response: AxiosResponse = await session.post( + toolUrl, + validatedPayload + ); + return response.data; + } catch (error) { + console.error( + `Error posting data to ${toolUrl}:`, + (error as any).response?.data || (error as any).message + ); + throw error; + } }; callable.toolName = name; + callable.description = description; + callable.params = paramSchema; callable.getName = function () { return this.toolName; }; + callable.getDescription = function () { + return this.description; + }; + callable.getParamSchema = function () { + return this.params; + }; return callable; } -export {ToolboxTool}; +export {ToolboxTool}; \ No newline at end of file diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 6efa736..0a4a966 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -13,12 +13,252 @@ // limitations under the License. import {ToolboxClient} from '../src/toolbox_core/client'; +import {ToolboxTool} from '../src/toolbox_core/tool'; +import { + ZodManifestSchema, + createZodObjectSchemaFromParameters, +} from '../src/toolbox_core/protocol'; +import axios, {AxiosInstance, AxiosResponse} from 'axios'; -const client = new ToolboxClient('https://some_base_url'); +// --- Mocking External Dependencies --- +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; -describe('getToolResponse', () => { - test('Should return a specific value based on inputs', async () => { - const response = await client.getToolResponse(3, 4); - expect(response).toBe(11); +jest.mock('../src/toolbox_core/tool', () => ({ + ToolboxTool: jest.fn(), +})); +const MockedToolboxToolFactory = ToolboxTool as jest.MockedFunction< + typeof ToolboxTool +>; + +jest.mock('../src/toolbox_core/protocol', () => ({ + ZodManifestSchema: { + safeParse: jest.fn(), + }, + createZodObjectSchemaFromParameters: jest.fn(), +})); +const MockedZodManifestSchema = ZodManifestSchema as jest.Mocked< + typeof ZodManifestSchema +>; +const MockedCreateZodObjectSchemaFromParameters = + createZodObjectSchemaFromParameters as jest.MockedFunction< + typeof createZodObjectSchemaFromParameters + >; + +// --- Test Helper Functions --- +type ApiErrorWithMessage = Error & {response?: {data: any}}; +const createApiError = ( + message: string, + responseData?: any +): ApiErrorWithMessage => { + const error = new Error(message) as ApiErrorWithMessage; + if (responseData !== undefined) { + error.response = {data: responseData}; + } + return error; +}; + +describe('ToolboxClient', () => { + const testBaseUrl = 'http://api.example.com'; + let consoleErrorSpy: jest.SpyInstance; + let mockSessionGet: jest.Mock; + + beforeEach(() => { + jest.resetAllMocks(); + + mockSessionGet = jest.fn(); + mockedAxios.create.mockReturnValue({ + get: mockSessionGet, + } as unknown as AxiosInstance); + + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + describe('constructor', () => { + it('should set baseUrl and create a new session if one is not provided', () => { + const client = new ToolboxClient(testBaseUrl); + + expect((client as any)._baseUrl).toBe(testBaseUrl); + expect(mockedAxios.create).toHaveBeenCalledTimes(1); + expect(mockedAxios.create).toHaveBeenCalledWith({baseURL: testBaseUrl}); + expect((client as any)._session.get).toBe(mockSessionGet); + }); + + it('should set baseUrl and use the provided session if one is given', () => { + const customMockSession = { + get: mockSessionGet, + } as unknown as AxiosInstance; + const client = new ToolboxClient(testBaseUrl, customMockSession); + + expect((client as any)._baseUrl).toBe(testBaseUrl); + expect((client as any)._session).toBe(customMockSession); + expect(mockedAxios.create).not.toHaveBeenCalled(); + }); + }); + + describe('loadTool', () => { + const toolName = 'calculator'; + const expectedApiUrl = `${testBaseUrl}/api/tool/${toolName}`; + let client: ToolboxClient; + + beforeEach(() => { + client = new ToolboxClient(testBaseUrl); + }); + + const setupMocksForSuccessfulLoad = ( + toolDefinition: object, + overrides: Partial<{ + manifestData: object; + zodParamsSchema: object; + toolInstance: object; + }> = {} + ) => { + const manifestData = overrides.manifestData || { + serverVersion: '1.0.0', + tools: {[toolName]: toolDefinition}, + }; + const zodParamsSchema = overrides.zodParamsSchema || { + _isMockZodParamSchema: true, + forTool: toolName, + }; + const toolInstance = overrides.toolInstance || { + _isMockTool: true, + loadedName: toolName, + }; + + mockSessionGet.mockResolvedValueOnce({ + data: manifestData, + } as AxiosResponse); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ + success: true, + data: manifestData, + } as any); + MockedCreateZodObjectSchemaFromParameters.mockReturnValueOnce( + zodParamsSchema as any + ); + MockedToolboxToolFactory.mockReturnValueOnce(toolInstance as any); + + return {manifestData, zodParamsSchema, toolInstance}; + }; + + it('should successfully load a tool with valid manifest and API response', async () => { + const mockToolDefinition = { + description: 'Performs calculations', + parameters: [ + {name: 'expression', type: 'string', description: 'Math expression'}, + ], + }; + + const {zodParamsSchema, toolInstance, manifestData} = + setupMocksForSuccessfulLoad(mockToolDefinition); + const loadedTool = await client.loadTool(toolName); + + expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); + expect(MockedZodManifestSchema.safeParse).toHaveBeenCalledWith( + manifestData + ); + expect(MockedCreateZodObjectSchemaFromParameters).toHaveBeenCalledWith( + mockToolDefinition.parameters + ); + expect(MockedToolboxToolFactory).toHaveBeenCalledWith( + (client as any)._session, + testBaseUrl, + toolName, + mockToolDefinition.description, + zodParamsSchema + ); + expect(loadedTool).toBe(toolInstance); + }); + + it('should throw an error if manifest parsing fails', async () => { + const mockApiResponseData = {invalid: 'manifest structure'}; + const mockZodErrorDetail = {message: 'Zod validation failed on manifest'}; + mockSessionGet.mockResolvedValueOnce({ + data: mockApiResponseData, + } as AxiosResponse); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ + success: false, + error: mockZodErrorDetail, + } as any); + + await expect(client.loadTool(toolName)).rejects.toThrow( + `Invalid manifest structure received: ${mockZodErrorDetail.message}` + ); + expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); + expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); + }); + + it('should throw an error if manifest.tools key is missing', async () => { + const mockManifestWithoutTools = {serverVersion: '1.0.0'}; // 'tools' key absent + setupMocksForSuccessfulLoad( + {description: '', parameters: []}, + {manifestData: mockManifestWithoutTools} + ); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ + success: true, + data: mockManifestWithoutTools, + } as any); + + await expect(client.loadTool(toolName)).rejects.toThrow( + `Tool "${toolName}" not found in manifest.` + ); + expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); + expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); + }); + + it('should throw an error if the specific tool is not found in manifest.tools', async () => { + const mockManifestWithOtherTools = { + serverVersion: '1.0.0', + tools: {anotherTool: {description: 'A different tool', parameters: []}}, + }; + mockSessionGet.mockResolvedValueOnce({ + data: mockManifestWithOtherTools, + } as AxiosResponse); + MockedZodManifestSchema.safeParse.mockReturnValueOnce({ + success: true, + data: mockManifestWithOtherTools, + } as any); + + await expect(client.loadTool(toolName)).rejects.toThrow( + `Tool "${toolName}" not found in manifest.` + ); + expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); + expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); + }); + + it('should throw and log error if API GET request fails with response data', async () => { + const errorResponseData = {code: 500, message: 'Server-side issue'}; + const apiError = createApiError( + 'API call failed unexpectedly', + errorResponseData + ); + mockSessionGet.mockRejectedValueOnce(apiError); + + await expect(client.loadTool(toolName)).rejects.toThrow(apiError); + expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error fetching data from ${expectedApiUrl}:`, + errorResponseData + ); + expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); + }); + + it('should throw and log error (using error.message) if API GET request fails without response data', async () => { + const errorMessage = 'Network unavailable'; + const apiError = createApiError(errorMessage); + mockSessionGet.mockRejectedValueOnce(apiError); + + await expect(client.loadTool(toolName)).rejects.toThrow(apiError); + expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error fetching data from ${expectedApiUrl}:`, + errorMessage + ); + expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); + }); }); -}); +}); \ No newline at end of file diff --git a/packages/toolbox-core/test/test.tool.ts b/packages/toolbox-core/test/test.tool.ts new file mode 100644 index 0000000..87bd5c1 --- /dev/null +++ b/packages/toolbox-core/test/test.tool.ts @@ -0,0 +1,318 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {ToolboxTool} from '../src/toolbox_core/tool'; +import {z, ZodObject} from 'zod'; +import {AxiosInstance, AxiosResponse} from 'axios'; + +// Global mocks for Axios +const mockAxiosPost = jest.fn(); +const mockSession = { + post: mockAxiosPost, +} as unknown as AxiosInstance; + +// Helper to create structured API error objects for testing +type ApiErrorWithMessage = Error & {response?: {data: any}}; +const createApiError = ( + message: string, + responseData?: any +): ApiErrorWithMessage => { + const error = new Error(message) as ApiErrorWithMessage; + if (responseData !== undefined) { + error.response = {data: responseData}; + } + return error; +}; + +describe('ToolboxTool', () => { + // Common constants for the tool + const baseURL = 'http://api.example.com'; + const toolName = 'myTestTool'; + const toolDescription = 'This is a description for the test tool.'; + + // Variables to be initialized in beforeEach + let basicParamSchema: ZodObject; + let consoleErrorSpy: jest.SpyInstance; + let tool: ReturnType; + + beforeEach(() => { + // Reset mocks before each test + mockAxiosPost.mockReset(); + + // Initialize a basic schema used by many tests + basicParamSchema = z.object({ + query: z.string().min(1, 'Query cannot be empty'), + limit: z.number().optional(), + }); + + // Spy on console.error to prevent logging and allow assertions + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + // Restore the original console.error + consoleErrorSpy.mockRestore(); + }); + + describe('Factory Properties and Getters', () => { + beforeEach(() => { + tool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); + }); + + it('should correctly assign toolName, description, and params to the callable function', () => { + expect(tool.toolName).toBe(toolName); + expect(tool.description).toBe(toolDescription); + expect(tool.params).toBe(basicParamSchema); + }); + + it('getName() should return the tool name', () => { + expect(tool.getName()).toBe(toolName); + }); + + it('getDescription() should return the tool description', () => { + expect(tool.getDescription()).toBe(toolDescription); + }); + + it('getParamSchema() should return the parameter schema', () => { + expect(tool.getParamSchema()).toBe(basicParamSchema); + }); + }); + + describe('Callable Function - Argument Validation', () => { + it('should call paramSchema.parse with the provided arguments', async () => { + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); + const parseSpy = jest.spyOn(basicParamSchema, 'parse'); + const callArgs = {query: 'test query'}; + mockAxiosPost.mockResolvedValueOnce({data: 'success'} as AxiosResponse); + + await currentTool(callArgs); + + expect(parseSpy).toHaveBeenCalledWith(callArgs); + parseSpy.mockRestore(); + }); + + it('should throw a formatted ZodError if argument validation fails', async () => { + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); + const invalidArgs = {query: ''}; // Fails because of empty string + + try { + await currentTool(invalidArgs); + throw new Error( + `Expected currentTool to throw a Zod validation error for tool "${toolName}", but it did not.` + ); + } catch (e: any) { + expect(e.message).toBe( + `Argument validation failed for tool "${toolName}":\n - query: Query cannot be empty` + ); + } + expect(mockAxiosPost).not.toHaveBeenCalled(); + }); + + it('should handle multiple ZodError issues in the validation error message', async () => { + const complexSchema = z.object({ + name: z.string().min(1, 'Name is required'), + age: z.number().positive('Age must be positive'), + }); + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + complexSchema + ); + const invalidArgs = {name: '', age: -5}; + + try { + await currentTool(invalidArgs); + throw new Error( + 'Expected currentTool to throw a Zod validation error, but it did not.' + ); + } catch (e: any) { + expect(e.message).toEqual( + expect.stringContaining( + `Argument validation failed for tool "${toolName}":` + ) + ); + expect(e.message).toEqual( + expect.stringContaining('name: Name is required') + ); + expect(e.message).toEqual( + expect.stringContaining('age: Age must be positive') + ); + } + expect(mockAxiosPost).not.toHaveBeenCalled(); + }); + + it('should throw a generic error if paramSchema.parse throws a non-ZodError', async () => { + const customError = new Error('A non-Zod parsing error occurred!'); + const failingSchema = { + parse: jest.fn().mockImplementation(() => { + throw customError; + }), + } as unknown as ZodObject; + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + failingSchema + ); + const callArgs = {query: 'some query'}; + + try { + await currentTool(callArgs); + throw new Error( + 'Expected currentTool to throw a non-Zod error during parsing, but it did not.' + ); + } catch (e: any) { + expect(e.message).toBe( + `Argument validation failed: ${String(customError)}` + ); + } + expect(mockAxiosPost).not.toHaveBeenCalled(); + }); + + it('should use an empty object as default if no arguments are provided and schema allows it', async () => { + const emptySchema = z.object({}); + const parseSpy = jest.spyOn(emptySchema, 'parse'); + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + emptySchema + ); + mockAxiosPost.mockResolvedValueOnce({data: 'success'}); + + await currentTool(); + + expect(parseSpy).toHaveBeenCalledWith({}); + expect(mockAxiosPost).toHaveBeenCalled(); + parseSpy.mockRestore(); + }); + + it('should fail validation if no arguments are given and schema requires them', async () => { + const currentTool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); + try { + await currentTool(); + throw new Error( + `Expected currentTool to throw a Zod validation error for tool "${toolName}" when no args provided, but it did not.` + ); + } catch (e: any) { + expect(e.message).toEqual( + expect.stringContaining( + 'Argument validation failed for tool "myTestTool":' + ) + ); + expect(e.message).toEqual(expect.stringContaining('query: Required')); + } + expect(mockAxiosPost).not.toHaveBeenCalled(); + }); + }); + + describe('Callable Function - API Call Execution', () => { + const validArgs = {query: 'search term', limit: 10}; + const expectedUrl = `${baseURL}/api/tool/${toolName}/invoke`; + const mockApiResponseData = {result: 'Data from API'}; + + beforeEach(() => { + tool = ToolboxTool( + mockSession, + baseURL, + toolName, + toolDescription, + basicParamSchema + ); + }); + + it('should make a POST request to the correct URL with the validated payload', async () => { + mockAxiosPost.mockResolvedValueOnce({ + data: mockApiResponseData, + } as AxiosResponse); + + const result = await tool(validArgs); + + expect(mockAxiosPost).toHaveBeenCalledTimes(1); + expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); + expect(result).toEqual(mockApiResponseData); + }); + + it('should re-throw the error and log to console.error if API call fails with response data', async () => { + const apiErrorResponseData = {error: 'detail from server'}; + const apiError = createApiError( + 'API request failed', + apiErrorResponseData + ); + mockAxiosPost.mockRejectedValueOnce(apiError); + + try { + await tool(validArgs); + throw new Error( + 'Expected tool call to throw an API error with response data, but it did not.' + ); + } catch (e: any) { + expect(e).toBe(apiError); + } + expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error posting data to ${expectedUrl}:`, + apiErrorResponseData + ); + }); + + it('should re-throw the error and log (using error.message) if API call fails without response data', async () => { + const apiErrorMessage = 'Network connection refused'; + const apiError = createApiError(apiErrorMessage); // No responseData + mockAxiosPost.mockRejectedValueOnce(apiError); + + try { + await tool(validArgs); + throw new Error( + 'Expected tool call to throw an API error without response data, but it did not.' + ); + } catch (e: any) { + expect(e).toBe(apiError); + } + expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `Error posting data to ${expectedUrl}:`, + apiErrorMessage + ); + }); + }); +}); \ No newline at end of file From 7544b356c5202321822e3fee2e32f7d252f6f252 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 18:55:46 +0530 Subject: [PATCH 29/57] lint --- packages/toolbox-core/src/toolbox_core/tool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index b94cdd8..fecd911 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -69,4 +69,4 @@ function ToolboxTool( return callable; } -export {ToolboxTool}; \ No newline at end of file +export {ToolboxTool}; From 94efad09631551acd3f9d06a0505d20f503a64fb Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 18:55:57 +0530 Subject: [PATCH 30/57] lint --- packages/toolbox-core/src/toolbox_core/client.ts | 2 +- packages/toolbox-core/test/test.client.ts | 2 +- packages/toolbox-core/test/test.tool.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index bf86e77..69da949 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -78,4 +78,4 @@ class ToolboxClient { } } -export {ToolboxClient}; \ No newline at end of file +export {ToolboxClient}; diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 0a4a966..3120841 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -261,4 +261,4 @@ describe('ToolboxClient', () => { expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); }); }); -}); \ No newline at end of file +}); diff --git a/packages/toolbox-core/test/test.tool.ts b/packages/toolbox-core/test/test.tool.ts index 87bd5c1..bebba58 100644 --- a/packages/toolbox-core/test/test.tool.ts +++ b/packages/toolbox-core/test/test.tool.ts @@ -315,4 +315,4 @@ describe('ToolboxTool', () => { ); }); }); -}); \ No newline at end of file +}); From b5fefd5ce2d5fb3413fd32c7633a2b871118ad04 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 20:02:05 +0530 Subject: [PATCH 31/57] lint --- packages/toolbox-core/test/test.client.ts | 46 ++++++++++++++++------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 3120841..4323577 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -18,7 +18,9 @@ import { ZodManifestSchema, createZodObjectSchemaFromParameters, } from '../src/toolbox_core/protocol'; -import axios, {AxiosInstance, AxiosResponse} from 'axios'; +import axios from 'axios'; +import type {AxiosInstance, AxiosResponse} from 'axios'; +import type {ZodObject, ZodRawShape} from 'zod'; // --- Mocking External Dependencies --- jest.mock('axios'); @@ -46,10 +48,10 @@ const MockedCreateZodObjectSchemaFromParameters = >; // --- Test Helper Functions --- -type ApiErrorWithMessage = Error & {response?: {data: any}}; +type ApiErrorWithMessage = Error & {response?: {data: unknown}}; const createApiError = ( message: string, - responseData?: any + responseData?: unknown ): ApiErrorWithMessage => { const error = new Error(message) as ApiErrorWithMessage; if (responseData !== undefined) { @@ -58,6 +60,16 @@ const createApiError = ( return error; }; +// Helper interfaces for Zod safeParse mock return values +interface MockSafeParseSuccess { + success: true; + data: T; +} +interface MockSafeParseError { + success: false; + error: {message: string}; // Simplified for test cases +} + describe('ToolboxClient', () => { const testBaseUrl = 'http://api.example.com'; let consoleErrorSpy: jest.SpyInstance; @@ -82,10 +94,12 @@ describe('ToolboxClient', () => { it('should set baseUrl and create a new session if one is not provided', () => { const client = new ToolboxClient(testBaseUrl); - expect((client as any)._baseUrl).toBe(testBaseUrl); + expect((client as {_baseUrl: string})._baseUrl).toBe(testBaseUrl); expect(mockedAxios.create).toHaveBeenCalledTimes(1); expect(mockedAxios.create).toHaveBeenCalledWith({baseURL: testBaseUrl}); - expect((client as any)._session.get).toBe(mockSessionGet); + expect((client as {_session: AxiosInstance})._session.get).toBe( + mockSessionGet + ); }); it('should set baseUrl and use the provided session if one is given', () => { @@ -94,8 +108,10 @@ describe('ToolboxClient', () => { } as unknown as AxiosInstance; const client = new ToolboxClient(testBaseUrl, customMockSession); - expect((client as any)._baseUrl).toBe(testBaseUrl); - expect((client as any)._session).toBe(customMockSession); + expect((client as {_baseUrl: string})._baseUrl).toBe(testBaseUrl); + expect((client as {_session: AxiosInstance})._session).toBe( + customMockSession + ); expect(mockedAxios.create).not.toHaveBeenCalled(); }); }); @@ -136,11 +152,13 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: manifestData, - } as any); + } as MockSafeParseSuccess); MockedCreateZodObjectSchemaFromParameters.mockReturnValueOnce( - zodParamsSchema as any + zodParamsSchema as ZodObject + ); + MockedToolboxToolFactory.mockReturnValueOnce( + toolInstance as ReturnType ); - MockedToolboxToolFactory.mockReturnValueOnce(toolInstance as any); return {manifestData, zodParamsSchema, toolInstance}; }; @@ -165,7 +183,7 @@ describe('ToolboxClient', () => { mockToolDefinition.parameters ); expect(MockedToolboxToolFactory).toHaveBeenCalledWith( - (client as any)._session, + (client as {_session: AxiosInstance})._session, testBaseUrl, toolName, mockToolDefinition.description, @@ -183,7 +201,7 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: false, error: mockZodErrorDetail, - } as any); + } as MockSafeParseError); await expect(client.loadTool(toolName)).rejects.toThrow( `Invalid manifest structure received: ${mockZodErrorDetail.message}` @@ -201,7 +219,7 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: mockManifestWithoutTools, - } as any); + } as MockSafeParseSuccess); await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` @@ -221,7 +239,7 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: mockManifestWithOtherTools, - } as any); + } as MockSafeParseSuccess); await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` From 2838188795433829528083901b327375d07f7b47 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 20:03:48 +0530 Subject: [PATCH 32/57] lint --- packages/toolbox-core/test/test.tool.ts | 42 +++++++++++++------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/toolbox-core/test/test.tool.ts b/packages/toolbox-core/test/test.tool.ts index bebba58..a119af7 100644 --- a/packages/toolbox-core/test/test.tool.ts +++ b/packages/toolbox-core/test/test.tool.ts @@ -13,7 +13,7 @@ // limitations under the License. import {ToolboxTool} from '../src/toolbox_core/tool'; -import {z, ZodObject} from 'zod'; +import {z, ZodObject, ZodRawShape} from 'zod'; import {AxiosInstance, AxiosResponse} from 'axios'; // Global mocks for Axios @@ -23,10 +23,10 @@ const mockSession = { } as unknown as AxiosInstance; // Helper to create structured API error objects for testing -type ApiErrorWithMessage = Error & {response?: {data: any}}; +type ApiErrorWithMessage = Error & {response?: {data: unknown}}; const createApiError = ( message: string, - responseData?: any + responseData?: unknown ): ApiErrorWithMessage => { const error = new Error(message) as ApiErrorWithMessage; if (responseData !== undefined) { @@ -42,7 +42,7 @@ describe('ToolboxTool', () => { const toolDescription = 'This is a description for the test tool.'; // Variables to be initialized in beforeEach - let basicParamSchema: ZodObject; + let basicParamSchema: ZodObject; let consoleErrorSpy: jest.SpyInstance; let tool: ReturnType; @@ -129,8 +129,8 @@ describe('ToolboxTool', () => { throw new Error( `Expected currentTool to throw a Zod validation error for tool "${toolName}", but it did not.` ); - } catch (e: any) { - expect(e.message).toBe( + } catch (e) { + expect((e as Error).message).toBe( `Argument validation failed for tool "${toolName}":\n - query: Query cannot be empty` ); } @@ -156,16 +156,16 @@ describe('ToolboxTool', () => { throw new Error( 'Expected currentTool to throw a Zod validation error, but it did not.' ); - } catch (e: any) { - expect(e.message).toEqual( + } catch (e) { + expect((e as Error).message).toEqual( expect.stringContaining( `Argument validation failed for tool "${toolName}":` ) ); - expect(e.message).toEqual( + expect((e as Error).message).toEqual( expect.stringContaining('name: Name is required') ); - expect(e.message).toEqual( + expect((e as Error).message).toEqual( expect.stringContaining('age: Age must be positive') ); } @@ -178,7 +178,7 @@ describe('ToolboxTool', () => { parse: jest.fn().mockImplementation(() => { throw customError; }), - } as unknown as ZodObject; + } as unknown as ZodObject; const currentTool = ToolboxTool( mockSession, baseURL, @@ -193,8 +193,8 @@ describe('ToolboxTool', () => { throw new Error( 'Expected currentTool to throw a non-Zod error during parsing, but it did not.' ); - } catch (e: any) { - expect(e.message).toBe( + } catch (e) { + expect((e as Error).message).toBe( `Argument validation failed: ${String(customError)}` ); } @@ -233,13 +233,15 @@ describe('ToolboxTool', () => { throw new Error( `Expected currentTool to throw a Zod validation error for tool "${toolName}" when no args provided, but it did not.` ); - } catch (e: any) { - expect(e.message).toEqual( + } catch (e) { + expect((e as Error).message).toEqual( expect.stringContaining( 'Argument validation failed for tool "myTestTool":' ) ); - expect(e.message).toEqual(expect.stringContaining('query: Required')); + expect((e as Error).message).toEqual( + expect.stringContaining('query: Required') + ); } expect(mockAxiosPost).not.toHaveBeenCalled(); }); @@ -285,8 +287,8 @@ describe('ToolboxTool', () => { throw new Error( 'Expected tool call to throw an API error with response data, but it did not.' ); - } catch (e: any) { - expect(e).toBe(apiError); + } catch (e) { + expect(e as ApiErrorWithMessage).toBe(apiError); } expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); expect(consoleErrorSpy).toHaveBeenCalledWith( @@ -305,8 +307,8 @@ describe('ToolboxTool', () => { throw new Error( 'Expected tool call to throw an API error without response data, but it did not.' ); - } catch (e: any) { - expect(e).toBe(apiError); + } catch (e) { + expect(e as ApiErrorWithMessage).toBe(apiError); } expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); expect(consoleErrorSpy).toHaveBeenCalledWith( From 601be0e88d4a898c203e8ba3909ef70a9e5feb10 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 20:25:02 +0530 Subject: [PATCH 33/57] fix test file --- packages/toolbox-core/test/test.client.ts | 48 +++++++---------------- 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 4323577..0a4a966 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -18,9 +18,7 @@ import { ZodManifestSchema, createZodObjectSchemaFromParameters, } from '../src/toolbox_core/protocol'; -import axios from 'axios'; -import type {AxiosInstance, AxiosResponse} from 'axios'; -import type {ZodObject, ZodRawShape} from 'zod'; +import axios, {AxiosInstance, AxiosResponse} from 'axios'; // --- Mocking External Dependencies --- jest.mock('axios'); @@ -48,10 +46,10 @@ const MockedCreateZodObjectSchemaFromParameters = >; // --- Test Helper Functions --- -type ApiErrorWithMessage = Error & {response?: {data: unknown}}; +type ApiErrorWithMessage = Error & {response?: {data: any}}; const createApiError = ( message: string, - responseData?: unknown + responseData?: any ): ApiErrorWithMessage => { const error = new Error(message) as ApiErrorWithMessage; if (responseData !== undefined) { @@ -60,16 +58,6 @@ const createApiError = ( return error; }; -// Helper interfaces for Zod safeParse mock return values -interface MockSafeParseSuccess { - success: true; - data: T; -} -interface MockSafeParseError { - success: false; - error: {message: string}; // Simplified for test cases -} - describe('ToolboxClient', () => { const testBaseUrl = 'http://api.example.com'; let consoleErrorSpy: jest.SpyInstance; @@ -94,12 +82,10 @@ describe('ToolboxClient', () => { it('should set baseUrl and create a new session if one is not provided', () => { const client = new ToolboxClient(testBaseUrl); - expect((client as {_baseUrl: string})._baseUrl).toBe(testBaseUrl); + expect((client as any)._baseUrl).toBe(testBaseUrl); expect(mockedAxios.create).toHaveBeenCalledTimes(1); expect(mockedAxios.create).toHaveBeenCalledWith({baseURL: testBaseUrl}); - expect((client as {_session: AxiosInstance})._session.get).toBe( - mockSessionGet - ); + expect((client as any)._session.get).toBe(mockSessionGet); }); it('should set baseUrl and use the provided session if one is given', () => { @@ -108,10 +94,8 @@ describe('ToolboxClient', () => { } as unknown as AxiosInstance; const client = new ToolboxClient(testBaseUrl, customMockSession); - expect((client as {_baseUrl: string})._baseUrl).toBe(testBaseUrl); - expect((client as {_session: AxiosInstance})._session).toBe( - customMockSession - ); + expect((client as any)._baseUrl).toBe(testBaseUrl); + expect((client as any)._session).toBe(customMockSession); expect(mockedAxios.create).not.toHaveBeenCalled(); }); }); @@ -152,13 +136,11 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: manifestData, - } as MockSafeParseSuccess); + } as any); MockedCreateZodObjectSchemaFromParameters.mockReturnValueOnce( - zodParamsSchema as ZodObject - ); - MockedToolboxToolFactory.mockReturnValueOnce( - toolInstance as ReturnType + zodParamsSchema as any ); + MockedToolboxToolFactory.mockReturnValueOnce(toolInstance as any); return {manifestData, zodParamsSchema, toolInstance}; }; @@ -183,7 +165,7 @@ describe('ToolboxClient', () => { mockToolDefinition.parameters ); expect(MockedToolboxToolFactory).toHaveBeenCalledWith( - (client as {_session: AxiosInstance})._session, + (client as any)._session, testBaseUrl, toolName, mockToolDefinition.description, @@ -201,7 +183,7 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: false, error: mockZodErrorDetail, - } as MockSafeParseError); + } as any); await expect(client.loadTool(toolName)).rejects.toThrow( `Invalid manifest structure received: ${mockZodErrorDetail.message}` @@ -219,7 +201,7 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: mockManifestWithoutTools, - } as MockSafeParseSuccess); + } as any); await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` @@ -239,7 +221,7 @@ describe('ToolboxClient', () => { MockedZodManifestSchema.safeParse.mockReturnValueOnce({ success: true, data: mockManifestWithOtherTools, - } as MockSafeParseSuccess); + } as any); await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` @@ -279,4 +261,4 @@ describe('ToolboxClient', () => { expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); }); }); -}); +}); \ No newline at end of file From 796e6fa2da0204faa54672cea58b1f9f8fac60f7 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Mon, 19 May 2025 20:26:11 +0530 Subject: [PATCH 34/57] lint --- packages/toolbox-core/test/test.client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 0a4a966..3120841 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -261,4 +261,4 @@ describe('ToolboxClient', () => { expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); }); }); -}); \ No newline at end of file +}); From decccee6d9c4f51b85e30b9a8f3c619a37e5deb7 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Tue, 20 May 2025 17:42:03 +0530 Subject: [PATCH 35/57] fix docstrings --- .../toolbox-core/src/toolbox_core/client.ts | 21 ++++++++++++++++--- .../toolbox-core/src/toolbox_core/tool.ts | 15 +++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index 69da949..bdbee6b 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -20,12 +20,20 @@ import { createZodObjectSchemaFromParameters, } from './protocol'; +/** + * An asynchronous client for interacting with a Toolbox service. + * Provides methods to discover and load tools defined by a remote Toolbox + * service endpoint. It manages an underlying client session. + */ class ToolboxClient { /** @private */ private _baseUrl: string; /** @private */ private _session: AxiosInstance; /** - * @param {string} url - The base URL for the Toolbox service API. + * Initializes the ToolboxClient. + * @param {string} url - The base URL for the Toolbox service API (e.g., "http://localhost:5000"). + * @param {AxiosInstance} [session] - Optional Axios instance for making HTTP + * requests. If not provided, a new one will be created. */ constructor(url: string, session?: AxiosInstance) { this._baseUrl = url; @@ -33,8 +41,15 @@ class ToolboxClient { } /** - * @param {string} toolName - Name of the tool. - * @returns {ToolboxTool} - A ToolboxTool instance. + * Asynchronously loads a tool from the server. + * Retrieves the schema for the specified tool from the Toolbox server and * returns a callable (`ToolboxTool`) that can be used to invoke the + * tool remotely. + * + * @param {string} toolName - The unique name or identifier of the tool to load. + * @returns {Promise>} A promise that resolves + * to a ToolboxTool instance, ready for execution. + * @throws {Error} If the tool is not found in the manifest, the manifest structure is invalid, + * or if there's an error fetching data from the API. */ async loadTool(toolName: string): Promise> { const url = `${this._baseUrl}/api/tool/${toolName}`; diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index fecd911..66b12b7 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -15,6 +15,21 @@ import {ZodObject, ZodError} from 'zod'; import {AxiosInstance, AxiosResponse} from 'axios'; +/** + * Creates a callable tool function representing a specific tool on a remote + * Toolbox server. + * + * @param {AxiosInstance} session - The Axios session for making HTTP requests. + * @param {string} baseUrl - The base URL of the Toolbox Server API. + * @param {string} name - The name of the remote tool. + * @param {string} description - A description of the remote tool. + * @param {ZodObject} paramSchema - The Zod schema for validating the tool's parameters. + * @returns {CallableTool & CallableToolProperties} An async function that, when + * called, invokes the tool with the provided arguments. Validates arguments + * against the tool's signature, then sends them + * as a JSON payload in a POST request to the tool's invoke URL. + */ + function ToolboxTool( session: AxiosInstance, baseUrl: string, From a36c2dbd705703c13f09cb611dabdd2c788a10f8 Mon Sep 17 00:00:00 2001 From: Twisha Bansal <58483338+twishabansal@users.noreply.github.com> Date: Tue, 20 May 2025 19:52:48 +0530 Subject: [PATCH 36/57] Update client.ts --- packages/toolbox-core/src/toolbox_core/client.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index bdbee6b..2606642 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -45,14 +45,14 @@ class ToolboxClient { * Retrieves the schema for the specified tool from the Toolbox server and * returns a callable (`ToolboxTool`) that can be used to invoke the * tool remotely. * - * @param {string} toolName - The unique name or identifier of the tool to load. + * @param {string} name - The unique name or identifier of the tool to load. * @returns {Promise>} A promise that resolves - * to a ToolboxTool instance, ready for execution. + * to a ToolboxTool function, ready for execution. * @throws {Error} If the tool is not found in the manifest, the manifest structure is invalid, * or if there's an error fetching data from the API. */ - async loadTool(toolName: string): Promise> { - const url = `${this._baseUrl}/api/tool/${toolName}`; + async loadTool(name: string): Promise> { + const url = `${this._baseUrl}/api/tool/${name}`; try { const response: AxiosResponse = await this._session.get(url); const responseData = response.data; @@ -62,21 +62,21 @@ class ToolboxClient { const manifest = manifestResponse.data; if ( manifest.tools && - Object.prototype.hasOwnProperty.call(manifest.tools, toolName) + Object.prototype.hasOwnProperty.call(manifest.tools, name) ) { - const specificToolSchema = manifest.tools[toolName]; + const specificToolSchema = manifest.tools[name]; const paramZodSchema = createZodObjectSchemaFromParameters( specificToolSchema.parameters ); return ToolboxTool( this._session, this._baseUrl, - toolName, + name, specificToolSchema.description, paramZodSchema ); } else { - throw new Error(`Tool "${toolName}" not found in manifest.`); + throw new Error(`Tool "${name}" not found in manifest.`); } } else { throw new Error( From 470dae738ae39eb2093d955e42eea111cb53b25e Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Wed, 21 May 2025 15:13:14 +0530 Subject: [PATCH 37/57] lint --- packages/toolbox-core/src/toolbox_core/client.ts | 5 +---- packages/toolbox-core/src/toolbox_core/tool.ts | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index 2606642..a91c595 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -84,10 +84,7 @@ class ToolboxClient { ); } } catch (error) { - console.error( - `Error fetching data from ${url}:`, - (error as any).response?.data || (error as any).message - ); + console.error(`Error fetching data from ${url}:`, error); throw error; } } diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index 66b12b7..7e65d8a 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -62,10 +62,7 @@ function ToolboxTool( ); return response.data; } catch (error) { - console.error( - `Error posting data to ${toolUrl}:`, - (error as any).response?.data || (error as any).message - ); + console.error(`Error posting data to ${toolUrl}:`, error); throw error; } }; From 9f3297f26ee1159823b8eb2deab44ecf59d9e909 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Wed, 21 May 2025 16:58:26 +0530 Subject: [PATCH 38/57] fix any type errors --- .../toolbox-core/src/toolbox_core/client.ts | 5 +- .../src/toolbox_core/errorUtils.ts | 42 +++++++ .../toolbox-core/src/toolbox_core/tool.ts | 10 +- packages/toolbox-core/test/test.client.ts | 37 +----- packages/toolbox-core/test/test.errorUtils.ts | 105 ++++++++++++++++++ packages/toolbox-core/test/test.tool.ts | 46 +------- 6 files changed, 162 insertions(+), 83 deletions(-) create mode 100644 packages/toolbox-core/src/toolbox_core/errorUtils.ts create mode 100644 packages/toolbox-core/test/test.errorUtils.ts diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index a91c595..9b911eb 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -14,11 +14,12 @@ import {ToolboxTool} from './tool'; import axios from 'axios'; -import type {AxiosInstance, AxiosResponse} from 'axios'; +import {type AxiosInstance, type AxiosResponse} from 'axios'; import { ZodManifestSchema, createZodObjectSchemaFromParameters, } from './protocol'; +import {logApiError} from './errorUtils'; // Import the new utility /** * An asynchronous client for interacting with a Toolbox service. @@ -84,7 +85,7 @@ class ToolboxClient { ); } } catch (error) { - console.error(`Error fetching data from ${url}:`, error); + logApiError(`Error fetching data from ${url}:`, error); throw error; } } diff --git a/packages/toolbox-core/src/toolbox_core/errorUtils.ts b/packages/toolbox-core/src/toolbox_core/errorUtils.ts new file mode 100644 index 0000000..e27f90f --- /dev/null +++ b/packages/toolbox-core/src/toolbox_core/errorUtils.ts @@ -0,0 +1,42 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {isAxiosError} from 'axios'; + +/** + * Logs a standardized error message to the console, differentiating between + * Axios errors with response data, Axios errors without response data (e.g., network errors), + * and other types of errors. + * + * @param {string} baseMessage - The base message to log, e.g., "Error fetching data from". + * @param {unknown} error - The error object caught. + */ +export function logApiError(baseMessage: string, error: unknown): void { + let loggableDetails: unknown; + + if (isAxiosError(error)) { + // Check if the error is from Axios and has response data + if (error.response && typeof error.response.data !== 'undefined') { + loggableDetails = error.response.data; + } else { + // Axios error without response data (e.g., network error, timeout) + loggableDetails = error.message; + } + } else if (error instanceof Error) { + loggableDetails = error.message; + } else { + loggableDetails = error; // Fallback for non-Error types or unknown errors + } + console.error(baseMessage, loggableDetails); +} diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index 7e65d8a..7ae2531 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {ZodObject, ZodError} from 'zod'; -import {AxiosInstance, AxiosResponse} from 'axios'; +import {ZodObject, ZodError, ZodRawShape} from 'zod'; +import {AxiosInstance, AxiosResponse, isAxiosError} from 'axios'; +import {logApiError} from './errorUtils'; /** * Creates a callable tool function representing a specific tool on a remote @@ -35,7 +36,7 @@ function ToolboxTool( baseUrl: string, name: string, description: string, - paramSchema: ZodObject + paramSchema: ZodObject ) { const toolUrl = `${baseUrl}/api/tool/${name}/invoke`; @@ -54,7 +55,6 @@ function ToolboxTool( } throw new Error(`Argument validation failed: ${String(error)}`); } - try { const response: AxiosResponse = await session.post( toolUrl, @@ -62,7 +62,7 @@ function ToolboxTool( ); return response.data; } catch (error) { - console.error(`Error posting data to ${toolUrl}:`, error); + logApiError(`Error posting data to ${toolUrl}:`, error); throw error; } }; diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 3120841..24480b2 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -45,19 +45,6 @@ const MockedCreateZodObjectSchemaFromParameters = typeof createZodObjectSchemaFromParameters >; -// --- Test Helper Functions --- -type ApiErrorWithMessage = Error & {response?: {data: any}}; -const createApiError = ( - message: string, - responseData?: any -): ApiErrorWithMessage => { - const error = new Error(message) as ApiErrorWithMessage; - if (responseData !== undefined) { - error.response = {data: responseData}; - } - return error; -}; - describe('ToolboxClient', () => { const testBaseUrl = 'http://api.example.com'; let consoleErrorSpy: jest.SpyInstance; @@ -230,33 +217,15 @@ describe('ToolboxClient', () => { expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); }); - it('should throw and log error if API GET request fails with response data', async () => { - const errorResponseData = {code: 500, message: 'Server-side issue'}; - const apiError = createApiError( - 'API call failed unexpectedly', - errorResponseData - ); - mockSessionGet.mockRejectedValueOnce(apiError); - - await expect(client.loadTool(toolName)).rejects.toThrow(apiError); - expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `Error fetching data from ${expectedApiUrl}:`, - errorResponseData - ); - expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); - }); - - it('should throw and log error (using error.message) if API GET request fails without response data', async () => { - const errorMessage = 'Network unavailable'; - const apiError = createApiError(errorMessage); + it('should throw and log error if API GET request fails', async () => { + const apiError = new Error('Server-side issue'); mockSessionGet.mockRejectedValueOnce(apiError); await expect(client.loadTool(toolName)).rejects.toThrow(apiError); expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); expect(consoleErrorSpy).toHaveBeenCalledWith( `Error fetching data from ${expectedApiUrl}:`, - errorMessage + 'Server-side issue' ); expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); }); diff --git a/packages/toolbox-core/test/test.errorUtils.ts b/packages/toolbox-core/test/test.errorUtils.ts new file mode 100644 index 0000000..a15a3a6 --- /dev/null +++ b/packages/toolbox-core/test/test.errorUtils.ts @@ -0,0 +1,105 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {logApiError} from '../src/toolbox_core/errorUtils'; +import {isAxiosError} from 'axios'; + +// Mock the 'axios' module, specifically the isAxiosError function +jest.mock('axios', () => ({ + ...jest.requireActual('axios'), // Import and retain default behavior + isAxiosError: jest.fn(), // Mock isAxiosError +})); + +describe('logApiError', () => { + let consoleErrorSpy: jest.SpyInstance; + const baseMessage = 'Test error:'; + + beforeEach(() => { + // Spy on console.error before each test + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + // Reset the mock for isAxiosError before each test + (isAxiosError as jest.MockedFunction).mockReset(); + }); + + afterEach(() => { + // Restore console.error to its original implementation after each test + consoleErrorSpy.mockRestore(); + }); + + it('should log error.response.data if error is AxiosError with response data', () => { + (isAxiosError as jest.MockedFunction).mockReturnValue(true); + const errorResponseData = {detail: 'API returned an error'}; + const mockError = { + isAxiosError: true, + response: {data: errorResponseData}, + message: 'Request failed with status code 500', + name: 'AxiosError', + config: {}, + code: 'ERR_BAD_RESPONSE', + }; + + logApiError(baseMessage, mockError); + + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledWith(baseMessage, errorResponseData); + }); + + it('should log error.message if error is AxiosError without response data', () => { + (isAxiosError as jest.MockedFunction).mockReturnValue(true); + const errorMessage = 'Network Error'; + const mockError = { + isAxiosError: true, + message: errorMessage, + name: 'AxiosError', + config: {}, + code: 'ERR_NETWORK', + }; // No error.response or error.response.data + + logApiError(baseMessage, mockError); + + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledWith(baseMessage, errorMessage); + }); + + it('should log error.message if error is a standard Error instance', () => { + (isAxiosError as jest.MockedFunction).mockReturnValue(false); + const errorMessage = 'This is a standard error'; + const mockError = new Error(errorMessage); + + logApiError(baseMessage, mockError); + + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledWith(baseMessage, errorMessage); + }); + + it('should log the error itself if it is a string', () => { + (isAxiosError as jest.MockedFunction).mockReturnValue(false); + const mockError = 'A simple string error'; + + logApiError(baseMessage, mockError); + + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledWith(baseMessage, mockError); + }); + + it('should log the error itself if it is a plain object (not Error or AxiosError)', () => { + (isAxiosError as jest.MockedFunction).mockReturnValue(false); + const mockError = {customError: 'Some custom error object'}; + + logApiError(baseMessage, mockError); + + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledWith(baseMessage, mockError); + }); +}); \ No newline at end of file diff --git a/packages/toolbox-core/test/test.tool.ts b/packages/toolbox-core/test/test.tool.ts index a119af7..e7756d3 100644 --- a/packages/toolbox-core/test/test.tool.ts +++ b/packages/toolbox-core/test/test.tool.ts @@ -22,19 +22,6 @@ const mockSession = { post: mockAxiosPost, } as unknown as AxiosInstance; -// Helper to create structured API error objects for testing -type ApiErrorWithMessage = Error & {response?: {data: unknown}}; -const createApiError = ( - message: string, - responseData?: unknown -): ApiErrorWithMessage => { - const error = new Error(message) as ApiErrorWithMessage; - if (responseData !== undefined) { - error.response = {data: responseData}; - } - return error; -}; - describe('ToolboxTool', () => { // Common constants for the tool const baseURL = 'http://api.example.com'; @@ -274,12 +261,8 @@ describe('ToolboxTool', () => { expect(result).toEqual(mockApiResponseData); }); - it('should re-throw the error and log to console.error if API call fails with response data', async () => { - const apiErrorResponseData = {error: 'detail from server'}; - const apiError = createApiError( - 'API request failed', - apiErrorResponseData - ); + it('should re-throw the error and log to console.error if API call fails', async () => { + const apiError = new Error('API request failed',); mockAxiosPost.mockRejectedValueOnce(apiError); try { @@ -288,32 +271,11 @@ describe('ToolboxTool', () => { 'Expected tool call to throw an API error with response data, but it did not.' ); } catch (e) { - expect(e as ApiErrorWithMessage).toBe(apiError); - } - expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); - expect(consoleErrorSpy).toHaveBeenCalledWith( - `Error posting data to ${expectedUrl}:`, - apiErrorResponseData - ); - }); - - it('should re-throw the error and log (using error.message) if API call fails without response data', async () => { - const apiErrorMessage = 'Network connection refused'; - const apiError = createApiError(apiErrorMessage); // No responseData - mockAxiosPost.mockRejectedValueOnce(apiError); - - try { - await tool(validArgs); - throw new Error( - 'Expected tool call to throw an API error without response data, but it did not.' - ); - } catch (e) { - expect(e as ApiErrorWithMessage).toBe(apiError); + expect(e as Error).toBe(apiError); } expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); expect(consoleErrorSpy).toHaveBeenCalledWith( - `Error posting data to ${expectedUrl}:`, - apiErrorMessage + `Error posting data to ${expectedUrl}:`, apiError.message ); }); }); From 93bf9107138e17862712843227ec70af4524facf Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Wed, 21 May 2025 17:25:50 +0530 Subject: [PATCH 39/57] lint --- .../toolbox-core/src/toolbox_core/tool.ts | 2 +- packages/toolbox-core/test/test.errorUtils.ts | 27 ++++++++++++++----- packages/toolbox-core/test/test.tool.ts | 5 ++-- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index 7ae2531..2652bd3 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -13,7 +13,7 @@ // limitations under the License. import {ZodObject, ZodError, ZodRawShape} from 'zod'; -import {AxiosInstance, AxiosResponse, isAxiosError} from 'axios'; +import {AxiosInstance, AxiosResponse} from 'axios'; import {logApiError} from './errorUtils'; /** diff --git a/packages/toolbox-core/test/test.errorUtils.ts b/packages/toolbox-core/test/test.errorUtils.ts index a15a3a6..bc0facc 100644 --- a/packages/toolbox-core/test/test.errorUtils.ts +++ b/packages/toolbox-core/test/test.errorUtils.ts @@ -38,7 +38,9 @@ describe('logApiError', () => { }); it('should log error.response.data if error is AxiosError with response data', () => { - (isAxiosError as jest.MockedFunction).mockReturnValue(true); + (isAxiosError as jest.MockedFunction).mockReturnValue( + true + ); const errorResponseData = {detail: 'API returned an error'}; const mockError = { isAxiosError: true, @@ -52,11 +54,16 @@ describe('logApiError', () => { logApiError(baseMessage, mockError); expect(consoleErrorSpy).toHaveBeenCalledTimes(1); - expect(consoleErrorSpy).toHaveBeenCalledWith(baseMessage, errorResponseData); + expect(consoleErrorSpy).toHaveBeenCalledWith( + baseMessage, + errorResponseData + ); }); it('should log error.message if error is AxiosError without response data', () => { - (isAxiosError as jest.MockedFunction).mockReturnValue(true); + (isAxiosError as jest.MockedFunction).mockReturnValue( + true + ); const errorMessage = 'Network Error'; const mockError = { isAxiosError: true, @@ -73,7 +80,9 @@ describe('logApiError', () => { }); it('should log error.message if error is a standard Error instance', () => { - (isAxiosError as jest.MockedFunction).mockReturnValue(false); + (isAxiosError as jest.MockedFunction).mockReturnValue( + false + ); const errorMessage = 'This is a standard error'; const mockError = new Error(errorMessage); @@ -84,7 +93,9 @@ describe('logApiError', () => { }); it('should log the error itself if it is a string', () => { - (isAxiosError as jest.MockedFunction).mockReturnValue(false); + (isAxiosError as jest.MockedFunction).mockReturnValue( + false + ); const mockError = 'A simple string error'; logApiError(baseMessage, mockError); @@ -94,7 +105,9 @@ describe('logApiError', () => { }); it('should log the error itself if it is a plain object (not Error or AxiosError)', () => { - (isAxiosError as jest.MockedFunction).mockReturnValue(false); + (isAxiosError as jest.MockedFunction).mockReturnValue( + false + ); const mockError = {customError: 'Some custom error object'}; logApiError(baseMessage, mockError); @@ -102,4 +115,4 @@ describe('logApiError', () => { expect(consoleErrorSpy).toHaveBeenCalledTimes(1); expect(consoleErrorSpy).toHaveBeenCalledWith(baseMessage, mockError); }); -}); \ No newline at end of file +}); diff --git a/packages/toolbox-core/test/test.tool.ts b/packages/toolbox-core/test/test.tool.ts index e7756d3..6fb5f2e 100644 --- a/packages/toolbox-core/test/test.tool.ts +++ b/packages/toolbox-core/test/test.tool.ts @@ -262,7 +262,7 @@ describe('ToolboxTool', () => { }); it('should re-throw the error and log to console.error if API call fails', async () => { - const apiError = new Error('API request failed',); + const apiError = new Error('API request failed'); mockAxiosPost.mockRejectedValueOnce(apiError); try { @@ -275,7 +275,8 @@ describe('ToolboxTool', () => { } expect(mockAxiosPost).toHaveBeenCalledWith(expectedUrl, validArgs); expect(consoleErrorSpy).toHaveBeenCalledWith( - `Error posting data to ${expectedUrl}:`, apiError.message + `Error posting data to ${expectedUrl}:`, + apiError.message ); }); }); From 42d6e57b8bd4a79ccf3fa94473322d8fc7ba8ad2 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Wed, 21 May 2025 17:30:53 +0530 Subject: [PATCH 40/57] clean --- packages/toolbox-core/src/toolbox_core/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index 9b911eb..7e29aca 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -19,7 +19,7 @@ import { ZodManifestSchema, createZodObjectSchemaFromParameters, } from './protocol'; -import {logApiError} from './errorUtils'; // Import the new utility +import {logApiError} from './errorUtils'; /** * An asynchronous client for interacting with a Toolbox service. From 7e363c3244d4b75f6148fbb210a31aa0d17bd685 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Wed, 21 May 2025 17:37:21 +0530 Subject: [PATCH 41/57] remove any --- packages/toolbox-core/test/test.client.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 24480b2..6ec9eba 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -69,10 +69,10 @@ describe('ToolboxClient', () => { it('should set baseUrl and create a new session if one is not provided', () => { const client = new ToolboxClient(testBaseUrl); - expect((client as any)._baseUrl).toBe(testBaseUrl); + expect(client['_baseUrl']).toBe(testBaseUrl); expect(mockedAxios.create).toHaveBeenCalledTimes(1); expect(mockedAxios.create).toHaveBeenCalledWith({baseURL: testBaseUrl}); - expect((client as any)._session.get).toBe(mockSessionGet); + expect(client['_session'].get).toBe(mockSessionGet); }); it('should set baseUrl and use the provided session if one is given', () => { @@ -81,8 +81,8 @@ describe('ToolboxClient', () => { } as unknown as AxiosInstance; const client = new ToolboxClient(testBaseUrl, customMockSession); - expect((client as any)._baseUrl).toBe(testBaseUrl); - expect((client as any)._session).toBe(customMockSession); + expect(client['_baseUrl']).toBe(testBaseUrl); + expect(client['_session']).toBe(customMockSession); expect(mockedAxios.create).not.toHaveBeenCalled(); }); }); @@ -152,7 +152,7 @@ describe('ToolboxClient', () => { mockToolDefinition.parameters ); expect(MockedToolboxToolFactory).toHaveBeenCalledWith( - (client as any)._session, + client['_session'], testBaseUrl, toolName, mockToolDefinition.description, From ce52779275936a2313c2c79ebfe4370ebca7d454 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Wed, 21 May 2025 18:20:34 +0530 Subject: [PATCH 42/57] change any to unknown --- packages/toolbox-core/src/toolbox_core/tool.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index 2652bd3..41c82aa 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -40,8 +40,8 @@ function ToolboxTool( ) { const toolUrl = `${baseUrl}/api/tool/${name}/invoke`; - const callable = async function (callArguments: Record = {}) { - let validatedPayload: Record; + const callable = async function (callArguments: Record = {}) { + let validatedPayload: Record; try { validatedPayload = paramSchema.parse(callArguments); } catch (error) { From 2f86750bc2f205dcdbbe50161e5fdb61bd857687 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Wed, 21 May 2025 18:22:36 +0530 Subject: [PATCH 43/57] lint --- packages/toolbox-core/src/toolbox_core/tool.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index 41c82aa..d2ae50e 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -40,7 +40,9 @@ function ToolboxTool( ) { const toolUrl = `${baseUrl}/api/tool/${name}/invoke`; - const callable = async function (callArguments: Record = {}) { + const callable = async function ( + callArguments: Record = {} + ) { let validatedPayload: Record; try { validatedPayload = paramSchema.parse(callArguments); From d8aaaa395d659e4ea176b11ba928d8d7f79e6de9 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Wed, 21 May 2025 22:13:18 +0530 Subject: [PATCH 44/57] remove unused deps --- packages/toolbox-core/package-lock.json | 266 +----------------------- packages/toolbox-core/package.json | 2 - 2 files changed, 2 insertions(+), 266 deletions(-) diff --git a/packages/toolbox-core/package-lock.json b/packages/toolbox-core/package-lock.json index 4052e2f..6cf4e61 100644 --- a/packages/toolbox-core/package-lock.json +++ b/packages/toolbox-core/package-lock.json @@ -9,8 +9,6 @@ "version": "0.1.0", "license": "Apache-2.0", "dependencies": { - "axios": "^1.9.0", - "axois": "^0.0.1-security", "zod": "^3.24.4" }, "devDependencies": { @@ -1703,28 +1701,6 @@ "dev": true, "license": "MIT" }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/axois": { - "version": "0.0.1-security", - "resolved": "https://registry.npmjs.org/axois/-/axois-0.0.1-security.tgz", - "integrity": "sha512-8Nui4fwwyxHfjAfpDlg3Jt66EJA4i1D1eJch3D+wM/Oe+qhpyp7yfiszko/O5/adYu20wc37RG9/Eg8QIJHcvA==" - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -1955,19 +1931,6 @@ "semver": "^7.0.0" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2160,18 +2123,6 @@ "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2329,15 +2280,6 @@ "node": ">=0.10.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2384,20 +2326,6 @@ "node": ">=6.0.0" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -2451,51 +2379,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3166,41 +3049,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3227,6 +3075,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3252,30 +3101,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -3286,19 +3111,6 @@ "node": ">=8.0.0" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -3368,18 +3180,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3447,37 +3247,11 @@ "node": ">=8" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4677,15 +4451,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/meow": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", @@ -4757,27 +4522,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -5272,12 +5016,6 @@ "node": ">= 6" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/packages/toolbox-core/package.json b/packages/toolbox-core/package.json index f08d9ed..7ac4e0e 100644 --- a/packages/toolbox-core/package.json +++ b/packages/toolbox-core/package.json @@ -46,8 +46,6 @@ "typescript": "^5.8.3" }, "dependencies": { - "axios": "^1.9.0", - "axois": "^0.0.1-security", "zod": "^3.24.4" } } From 5213b3380723b746dbef4c6cdd5081b0fdb196e9 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Wed, 21 May 2025 22:17:21 +0530 Subject: [PATCH 45/57] move deps to correct pr --- packages/toolbox-core/package-lock.json | 260 +++++++++++++++++++++++- packages/toolbox-core/package.json | 1 + 2 files changed, 259 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-core/package-lock.json b/packages/toolbox-core/package-lock.json index 6cf4e61..4c6b251 100644 --- a/packages/toolbox-core/package-lock.json +++ b/packages/toolbox-core/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "Apache-2.0", "dependencies": { + "axios": "^1.9.0", "zod": "^3.24.4" }, "devDependencies": { @@ -1701,6 +1702,23 @@ "dev": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -1931,6 +1949,19 @@ "semver": "^7.0.0" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2123,6 +2154,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2280,6 +2323,15 @@ "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2326,6 +2378,20 @@ "node": ">=6.0.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -2379,6 +2445,51 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3049,6 +3160,41 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3075,7 +3221,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3101,6 +3246,30 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -3111,6 +3280,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -3180,6 +3362,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3247,11 +3441,37 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4451,6 +4671,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/meow": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", @@ -4522,6 +4751,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -5016,6 +5266,12 @@ "node": ">= 6" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/packages/toolbox-core/package.json b/packages/toolbox-core/package.json index 7ac4e0e..6b72088 100644 --- a/packages/toolbox-core/package.json +++ b/packages/toolbox-core/package.json @@ -46,6 +46,7 @@ "typescript": "^5.8.3" }, "dependencies": { + "axios": "^1.9.0", "zod": "^3.24.4" } } From 2b6a31942ac19ff3a23ca5846a5e4142c4fb6123 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 22 May 2025 15:15:16 +0530 Subject: [PATCH 46/57] rename methods --- packages/toolbox-core/src/toolbox_core/protocol.ts | 8 ++++---- packages/toolbox-core/test/test.protocol.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/protocol.ts b/packages/toolbox-core/src/toolbox_core/protocol.ts index 05746f6..f6e13f8 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.ts +++ b/packages/toolbox-core/src/toolbox_core/protocol.ts @@ -98,7 +98,7 @@ export const ZodManifestSchema = z.object({ * @param param The ParameterSchema (TypeScript type) to convert. * @returns A ZodTypeAny representing the schema for this parameter. */ -function buildZodShapeFromParameter(param: ParameterSchema): ZodTypeAny { +function buildZodShapeFromParam(param: ParameterSchema): ZodTypeAny { switch (param.type) { case 'string': return z.string(); @@ -110,7 +110,7 @@ function buildZodShapeFromParameter(param: ParameterSchema): ZodTypeAny { return z.boolean(); case 'array': // Recursively build the schema for array items - return z.array(buildZodShapeFromParameter(param.items)); + return z.array(buildZodShapeFromParam(param.items)); default: { // This ensures exhaustiveness at compile time if ParameterSchema is a discriminated union const _exhaustiveCheck: never = param; @@ -125,12 +125,12 @@ function buildZodShapeFromParameter(param: ParameterSchema): ZodTypeAny { * @param params Array of ParameterSchema objects. * @returns A ZodObject schema. */ -export function createZodObjectSchemaFromParameters( +export function createZodSchemaFromParams( params: ParameterSchema[] ): ZodObject { const shape: ZodRawShape = {}; for (const param of params) { - shape[param.name] = buildZodShapeFromParameter(param); + shape[param.name] = buildZodShapeFromParam(param); } return z.object(shape).strict(); } diff --git a/packages/toolbox-core/test/test.protocol.ts b/packages/toolbox-core/test/test.protocol.ts index 4a80be9..4811fe7 100644 --- a/packages/toolbox-core/test/test.protocol.ts +++ b/packages/toolbox-core/test/test.protocol.ts @@ -18,7 +18,7 @@ import { ZodToolSchema, ZodManifestSchema, ParameterSchema, - createZodObjectSchemaFromParameters, + createZodSchemaFromParams, } from '../src/toolbox_core/protocol'; // HELPER FUNCTIONS @@ -275,7 +275,7 @@ describe('ZodManifestSchema', () => { describe('createZodObjectSchemaFromParameters', () => { it('should create an empty Zod object for an empty parameters array (and be strict)', () => { const params: ParameterSchema[] = []; - const schema = createZodObjectSchemaFromParameters(params); + const schema = createZodSchemaFromParams(params); expectParseSuccess(schema, {}); expectParseFailure(schema, {anyKey: 'anyValue'}, errors => { @@ -295,7 +295,7 @@ describe('createZodObjectSchemaFromParameters', () => { {name: 'age', description: 'User age', type: 'integer' as const}, {name: 'isActive', description: 'User status', type: 'boolean' as const}, ]; - const schema = createZodObjectSchemaFromParameters(params); + const schema = createZodSchemaFromParams(params); expectParseSuccess(schema, {username: 'john_doe', age: 30, isActive: true}); @@ -324,7 +324,7 @@ describe('createZodObjectSchemaFromParameters', () => { }, {name: 'id', description: 'An identifier', type: 'integer' as const}, ]; - const schema = createZodObjectSchemaFromParameters(params); + const schema = createZodSchemaFromParams(params); expectParseSuccess(schema, {tags: ['news', 'tech'], id: 1}); @@ -351,7 +351,7 @@ describe('createZodObjectSchemaFromParameters', () => { }, }, ]; - const schema = createZodObjectSchemaFromParameters(params); + const schema = createZodSchemaFromParams(params); expectParseSuccess(schema, { matrix: [ @@ -385,7 +385,7 @@ describe('createZodObjectSchemaFromParameters', () => { } as unknown as ParameterSchema, ]; expect(() => - createZodObjectSchemaFromParameters(paramsWithUnknownType) + createZodSchemaFromParams(paramsWithUnknownType) ).toThrow('Unknown parameter type: someUnrecognizedType'); }); }); From d964e2348aa3e7e6c7b9dff42e613f3e4ea47949 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 22 May 2025 15:16:28 +0530 Subject: [PATCH 47/57] lint --- packages/toolbox-core/test/test.protocol.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/test/test.protocol.ts b/packages/toolbox-core/test/test.protocol.ts index 4811fe7..c112621 100644 --- a/packages/toolbox-core/test/test.protocol.ts +++ b/packages/toolbox-core/test/test.protocol.ts @@ -384,8 +384,8 @@ describe('createZodObjectSchemaFromParameters', () => { type: 'someUnrecognizedType', } as unknown as ParameterSchema, ]; - expect(() => - createZodSchemaFromParams(paramsWithUnknownType) - ).toThrow('Unknown parameter type: someUnrecognizedType'); + expect(() => createZodSchemaFromParams(paramsWithUnknownType)).toThrow( + 'Unknown parameter type: someUnrecognizedType' + ); }); }); From 1df348a9052dfb45e2e35ffa6695d0c9dac9baee Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 22 May 2025 15:27:18 +0530 Subject: [PATCH 48/57] fix method name --- packages/toolbox-core/src/toolbox_core/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index 7e29aca..2b807a0 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -17,7 +17,7 @@ import axios from 'axios'; import {type AxiosInstance, type AxiosResponse} from 'axios'; import { ZodManifestSchema, - createZodObjectSchemaFromParameters, + createZodSchemaFromParams, } from './protocol'; import {logApiError} from './errorUtils'; @@ -66,7 +66,7 @@ class ToolboxClient { Object.prototype.hasOwnProperty.call(manifest.tools, name) ) { const specificToolSchema = manifest.tools[name]; - const paramZodSchema = createZodObjectSchemaFromParameters( + const paramZodSchema = createZodSchemaFromParams( specificToolSchema.parameters ); return ToolboxTool( From 72d662bd02ed925b43f4736fe827d1b7227bfa68 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 22 May 2025 15:29:48 +0530 Subject: [PATCH 49/57] rename methods --- .../toolbox-core/src/toolbox_core/client.ts | 5 +---- packages/toolbox-core/test/test.client.ts | 20 +++++++++---------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index 2b807a0..88e0e18 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -15,10 +15,7 @@ import {ToolboxTool} from './tool'; import axios from 'axios'; import {type AxiosInstance, type AxiosResponse} from 'axios'; -import { - ZodManifestSchema, - createZodSchemaFromParams, -} from './protocol'; +import {ZodManifestSchema, createZodSchemaFromParams} from './protocol'; import {logApiError} from './errorUtils'; /** diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 6ec9eba..b07b5b7 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -16,7 +16,7 @@ import {ToolboxClient} from '../src/toolbox_core/client'; import {ToolboxTool} from '../src/toolbox_core/tool'; import { ZodManifestSchema, - createZodObjectSchemaFromParameters, + createZodSchemaFromParams, } from '../src/toolbox_core/protocol'; import axios, {AxiosInstance, AxiosResponse} from 'axios'; @@ -35,14 +35,14 @@ jest.mock('../src/toolbox_core/protocol', () => ({ ZodManifestSchema: { safeParse: jest.fn(), }, - createZodObjectSchemaFromParameters: jest.fn(), + createZodSchemaFromParams: jest.fn(), })); const MockedZodManifestSchema = ZodManifestSchema as jest.Mocked< typeof ZodManifestSchema >; -const MockedCreateZodObjectSchemaFromParameters = - createZodObjectSchemaFromParameters as jest.MockedFunction< - typeof createZodObjectSchemaFromParameters +const MockedCreateZodSchemaFromParams = + createZodSchemaFromParams as jest.MockedFunction< + typeof createZodSchemaFromParams >; describe('ToolboxClient', () => { @@ -124,7 +124,7 @@ describe('ToolboxClient', () => { success: true, data: manifestData, } as any); - MockedCreateZodObjectSchemaFromParameters.mockReturnValueOnce( + MockedCreateZodSchemaFromParams.mockReturnValueOnce( zodParamsSchema as any ); MockedToolboxToolFactory.mockReturnValueOnce(toolInstance as any); @@ -148,7 +148,7 @@ describe('ToolboxClient', () => { expect(MockedZodManifestSchema.safeParse).toHaveBeenCalledWith( manifestData ); - expect(MockedCreateZodObjectSchemaFromParameters).toHaveBeenCalledWith( + expect(MockedCreateZodSchemaFromParams).toHaveBeenCalledWith( mockToolDefinition.parameters ); expect(MockedToolboxToolFactory).toHaveBeenCalledWith( @@ -175,7 +175,7 @@ describe('ToolboxClient', () => { await expect(client.loadTool(toolName)).rejects.toThrow( `Invalid manifest structure received: ${mockZodErrorDetail.message}` ); - expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); + expect(MockedCreateZodSchemaFromParams).not.toHaveBeenCalled(); expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); }); @@ -193,7 +193,7 @@ describe('ToolboxClient', () => { await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` ); - expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); + expect(MockedCreateZodSchemaFromParams).not.toHaveBeenCalled(); expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); }); @@ -213,7 +213,7 @@ describe('ToolboxClient', () => { await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` ); - expect(MockedCreateZodObjectSchemaFromParameters).not.toHaveBeenCalled(); + expect(MockedCreateZodSchemaFromParams).not.toHaveBeenCalled(); expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); }); From 9b7592ee9ec4ff18cac738a61834d73afcf9ce2d Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 22 May 2025 17:24:08 +0530 Subject: [PATCH 50/57] resolve comments --- packages/toolbox-core/src/toolbox_core/client.ts | 5 ++--- packages/toolbox-core/src/toolbox_core/tool.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index 88e0e18..fe2d434 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -20,8 +20,6 @@ import {logApiError} from './errorUtils'; /** * An asynchronous client for interacting with a Toolbox service. - * Provides methods to discover and load tools defined by a remote Toolbox - * service endpoint. It manages an underlying client session. */ class ToolboxClient { /** @private */ private _baseUrl: string; @@ -40,7 +38,8 @@ class ToolboxClient { /** * Asynchronously loads a tool from the server. - * Retrieves the schema for the specified tool from the Toolbox server and * returns a callable (`ToolboxTool`) that can be used to invoke the + * Retrieves the schema for the specified tool from the Toolbox server and + * returns a callable (`ToolboxTool`) that can be used to invoke the * tool remotely. * * @param {string} name - The unique name or identifier of the tool to load. diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index d2ae50e..883753b 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -45,7 +45,7 @@ function ToolboxTool( ) { let validatedPayload: Record; try { - validatedPayload = paramSchema.parse(callArguments); + validatedPayload = paramSchema.safeParse(callArguments); } catch (error) { if (error instanceof ZodError) { const errorMessages = error.errors.map( From f51f4c56fc38a1ab9c785b43bb18df132e81a856 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 22 May 2025 17:25:42 +0530 Subject: [PATCH 51/57] lint --- packages/toolbox-core/src/toolbox_core/client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index fe2d434..2592b8c 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -22,8 +22,8 @@ import {logApiError} from './errorUtils'; * An asynchronous client for interacting with a Toolbox service. */ class ToolboxClient { - /** @private */ private _baseUrl: string; - /** @private */ private _session: AxiosInstance; + private _baseUrl: string; + private _session: AxiosInstance; /** * Initializes the ToolboxClient. @@ -38,7 +38,7 @@ class ToolboxClient { /** * Asynchronously loads a tool from the server. - * Retrieves the schema for the specified tool from the Toolbox server and + * Retrieves the schema for the specified tool from the Toolbox server and * returns a callable (`ToolboxTool`) that can be used to invoke the * tool remotely. * From 71aaeda6a6b9921717e78c615467af621af1749b Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 22 May 2025 17:32:27 +0530 Subject: [PATCH 52/57] use parse instead of safeparse --- packages/toolbox-core/src/toolbox_core/tool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/toolbox-core/src/toolbox_core/tool.ts b/packages/toolbox-core/src/toolbox_core/tool.ts index 883753b..d2ae50e 100644 --- a/packages/toolbox-core/src/toolbox_core/tool.ts +++ b/packages/toolbox-core/src/toolbox_core/tool.ts @@ -45,7 +45,7 @@ function ToolboxTool( ) { let validatedPayload: Record; try { - validatedPayload = paramSchema.safeParse(callArguments); + validatedPayload = paramSchema.parse(callArguments); } catch (error) { if (error instanceof ZodError) { const errorMessages = error.errors.map( From 1b261171a0b3604d0be59d0b6812da310e2a18b5 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 22 May 2025 17:42:16 +0530 Subject: [PATCH 53/57] use parse instead of safeparse --- .../toolbox-core/src/toolbox_core/client.ts | 14 ++-- .../toolbox-core/src/toolbox_core/protocol.ts | 2 + packages/toolbox-core/test/test.client.ts | 64 +++++++++---------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index 2592b8c..ba34c6f 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -54,9 +54,8 @@ class ToolboxClient { const response: AxiosResponse = await this._session.get(url); const responseData = response.data; - const manifestResponse = ZodManifestSchema.safeParse(responseData); - if (manifestResponse.success) { - const manifest = manifestResponse.data; + try { + const manifest = ZodManifestSchema.parse(responseData); if ( manifest.tools && Object.prototype.hasOwnProperty.call(manifest.tools, name) @@ -75,9 +74,14 @@ class ToolboxClient { } else { throw new Error(`Tool "${name}" not found in manifest.`); } - } else { + } catch (validationError) { + if (validationError instanceof Error) { + throw new Error( + `Invalid manifest structure received: ${validationError.message}` + ); + } throw new Error( - `Invalid manifest structure received: ${manifestResponse.error.message}` + 'Invalid manifest structure received: Unknown validation error.' ); } } catch (error) { diff --git a/packages/toolbox-core/src/toolbox_core/protocol.ts b/packages/toolbox-core/src/toolbox_core/protocol.ts index f6e13f8..80e6d21 100644 --- a/packages/toolbox-core/src/toolbox_core/protocol.ts +++ b/packages/toolbox-core/src/toolbox_core/protocol.ts @@ -93,6 +93,8 @@ export const ZodManifestSchema = z.object({ ), }); +export type ZodManifest = z.infer; + /** * Recursively builds a Zod schema for a single parameter based on its TypeScript definition. * @param param The ParameterSchema (TypeScript type) to convert. diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index b07b5b7..57718a0 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -17,6 +17,7 @@ import {ToolboxTool} from '../src/toolbox_core/tool'; import { ZodManifestSchema, createZodSchemaFromParams, + type ZodManifest, } from '../src/toolbox_core/protocol'; import axios, {AxiosInstance, AxiosResponse} from 'axios'; @@ -33,7 +34,7 @@ const MockedToolboxToolFactory = ToolboxTool as jest.MockedFunction< jest.mock('../src/toolbox_core/protocol', () => ({ ZodManifestSchema: { - safeParse: jest.fn(), + parse: jest.fn(), }, createZodSchemaFromParams: jest.fn(), })); @@ -98,16 +99,17 @@ describe('ToolboxClient', () => { const setupMocksForSuccessfulLoad = ( toolDefinition: object, - overrides: Partial<{ - manifestData: object; - zodParamsSchema: object; - toolInstance: object; - }> = {} + overrides: { + manifestData?: Partial; // Use a more specific type or Partial + zodParamsSchema?: object; + toolInstance?: object; + } = {} ) => { - const manifestData = overrides.manifestData || { + const manifestData: ZodManifest = { serverVersion: '1.0.0', tools: {[toolName]: toolDefinition}, - }; + ...overrides.manifestData, // Allow overriding parts of the manifest + } as ZodManifest; // Cast to ensure type compatibility if toolDefinition is generic const zodParamsSchema = overrides.zodParamsSchema || { _isMockZodParamSchema: true, forTool: toolName, @@ -120,10 +122,7 @@ describe('ToolboxClient', () => { mockSessionGet.mockResolvedValueOnce({ data: manifestData, } as AxiosResponse); - MockedZodManifestSchema.safeParse.mockReturnValueOnce({ - success: true, - data: manifestData, - } as any); + MockedZodManifestSchema.parse.mockReturnValueOnce(manifestData); MockedCreateZodSchemaFromParams.mockReturnValueOnce( zodParamsSchema as any ); @@ -145,9 +144,7 @@ describe('ToolboxClient', () => { const loadedTool = await client.loadTool(toolName); expect(mockSessionGet).toHaveBeenCalledWith(expectedApiUrl); - expect(MockedZodManifestSchema.safeParse).toHaveBeenCalledWith( - manifestData - ); + expect(MockedZodManifestSchema.parse).toHaveBeenCalledWith(manifestData); expect(MockedCreateZodSchemaFromParams).toHaveBeenCalledWith( mockToolDefinition.parameters ); @@ -163,17 +160,17 @@ describe('ToolboxClient', () => { it('should throw an error if manifest parsing fails', async () => { const mockApiResponseData = {invalid: 'manifest structure'}; - const mockZodErrorDetail = {message: 'Zod validation failed on manifest'}; + const mockZodError = new Error('Zod validation failed on manifest'); + mockSessionGet.mockResolvedValueOnce({ data: mockApiResponseData, } as AxiosResponse); - MockedZodManifestSchema.safeParse.mockReturnValueOnce({ - success: false, - error: mockZodErrorDetail, - } as any); + MockedZodManifestSchema.parse.mockImplementationOnce(() => { + throw mockZodError; + }); await expect(client.loadTool(toolName)).rejects.toThrow( - `Invalid manifest structure received: ${mockZodErrorDetail.message}` + `Invalid manifest structure received: ${mockZodError.message}` ); expect(MockedCreateZodSchemaFromParams).not.toHaveBeenCalled(); expect(MockedToolboxToolFactory).not.toHaveBeenCalled(); @@ -181,14 +178,13 @@ describe('ToolboxClient', () => { it('should throw an error if manifest.tools key is missing', async () => { const mockManifestWithoutTools = {serverVersion: '1.0.0'}; // 'tools' key absent - setupMocksForSuccessfulLoad( - {description: '', parameters: []}, - {manifestData: mockManifestWithoutTools} - ); - MockedZodManifestSchema.safeParse.mockReturnValueOnce({ - success: true, + + mockSessionGet.mockResolvedValueOnce({ data: mockManifestWithoutTools, - } as any); + } as AxiosResponse); + MockedZodManifestSchema.parse.mockReturnValueOnce( + mockManifestWithoutTools as any + ); await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` @@ -201,15 +197,13 @@ describe('ToolboxClient', () => { const mockManifestWithOtherTools = { serverVersion: '1.0.0', tools: {anotherTool: {description: 'A different tool', parameters: []}}, - }; + } as ZodManifest; // Ensure this mock data conforms to ZodManifest mockSessionGet.mockResolvedValueOnce({ data: mockManifestWithOtherTools, } as AxiosResponse); - MockedZodManifestSchema.safeParse.mockReturnValueOnce({ - success: true, - data: mockManifestWithOtherTools, - } as any); - + MockedZodManifestSchema.parse.mockReturnValueOnce( + mockManifestWithOtherTools as any + ); await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` ); @@ -227,7 +221,7 @@ describe('ToolboxClient', () => { `Error fetching data from ${expectedApiUrl}:`, 'Server-side issue' ); - expect(MockedZodManifestSchema.safeParse).not.toHaveBeenCalled(); + expect(MockedZodManifestSchema.parse).not.toHaveBeenCalled(); }); }); }); From 8c2aa81535f171d946f65b22d408ecfb2622be87 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 22 May 2025 18:22:12 +0530 Subject: [PATCH 54/57] any issue fix --- packages/toolbox-core/test/test.client.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 57718a0..3e353f7 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -100,7 +100,7 @@ describe('ToolboxClient', () => { const setupMocksForSuccessfulLoad = ( toolDefinition: object, overrides: { - manifestData?: Partial; // Use a more specific type or Partial + manifestData?: Partial; zodParamsSchema?: object; toolInstance?: object; } = {} @@ -109,7 +109,7 @@ describe('ToolboxClient', () => { serverVersion: '1.0.0', tools: {[toolName]: toolDefinition}, ...overrides.manifestData, // Allow overriding parts of the manifest - } as ZodManifest; // Cast to ensure type compatibility if toolDefinition is generic + } as ZodManifest; const zodParamsSchema = overrides.zodParamsSchema || { _isMockZodParamSchema: true, forTool: toolName, @@ -183,7 +183,7 @@ describe('ToolboxClient', () => { data: mockManifestWithoutTools, } as AxiosResponse); MockedZodManifestSchema.parse.mockReturnValueOnce( - mockManifestWithoutTools as any + mockManifestWithoutTools as unknown as ZodManifest ); await expect(client.loadTool(toolName)).rejects.toThrow( @@ -197,12 +197,12 @@ describe('ToolboxClient', () => { const mockManifestWithOtherTools = { serverVersion: '1.0.0', tools: {anotherTool: {description: 'A different tool', parameters: []}}, - } as ZodManifest; // Ensure this mock data conforms to ZodManifest + } as ZodManifest; mockSessionGet.mockResolvedValueOnce({ data: mockManifestWithOtherTools, } as AxiosResponse); MockedZodManifestSchema.parse.mockReturnValueOnce( - mockManifestWithOtherTools as any + mockManifestWithOtherTools as ZodManifest ); await expect(client.loadTool(toolName)).rejects.toThrow( `Tool "${toolName}" not found in manifest.` From a4dcea2a0ba80474d277194b2745206b11ecaccd Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 22 May 2025 18:24:33 +0530 Subject: [PATCH 55/57] any lint --- packages/toolbox-core/test/test.client.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 3e353f7..0792a48 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -20,6 +20,8 @@ import { type ZodManifest, } from '../src/toolbox_core/protocol'; import axios, {AxiosInstance, AxiosResponse} from 'axios'; +import { ZodRawShape, ZodObject} from 'zod'; + // --- Mocking External Dependencies --- jest.mock('axios'); @@ -124,7 +126,7 @@ describe('ToolboxClient', () => { } as AxiosResponse); MockedZodManifestSchema.parse.mockReturnValueOnce(manifestData); MockedCreateZodSchemaFromParams.mockReturnValueOnce( - zodParamsSchema as any + zodParamsSchema as ZodObject ); MockedToolboxToolFactory.mockReturnValueOnce(toolInstance as any); From deb576a379a2e87c43480e7f15e472bd95c05eb7 Mon Sep 17 00:00:00 2001 From: Twisha Bansal Date: Thu, 22 May 2025 18:47:34 +0530 Subject: [PATCH 56/57] fix any type issue --- packages/toolbox-core/test/test.client.ts | 88 ++++++++++++++++++----- 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/packages/toolbox-core/test/test.client.ts b/packages/toolbox-core/test/test.client.ts index 0792a48..611d841 100644 --- a/packages/toolbox-core/test/test.client.ts +++ b/packages/toolbox-core/test/test.client.ts @@ -20,8 +20,29 @@ import { type ZodManifest, } from '../src/toolbox_core/protocol'; import axios, {AxiosInstance, AxiosResponse} from 'axios'; -import { ZodRawShape, ZodObject} from 'zod'; +import {ZodRawShape, ZodObject, ZodTypeAny} from 'zod'; +// --- Helper Types --- +type OriginalToolboxToolType = + typeof import('../src/toolbox_core/tool').ToolboxTool; + +type CallableToolReturnedByFactory = ReturnType; + +const createMockZodObject = ( + shape: ZodRawShape = {} +): ZodObject => + ({ + parse: jest.fn(args => args), // Simple pass-through + safeParse: jest.fn(args => ({success: true, data: args})), + _def: { + typeName: 'ZodObject', + shape: () => shape, + }, + shape: shape, + pick: jest.fn().mockReturnThis(), + omit: jest.fn().mockReturnThis(), + extend: jest.fn().mockReturnThis(), + }) as unknown as ZodObject; // --- Mocking External Dependencies --- jest.mock('axios'); @@ -30,9 +51,9 @@ const mockedAxios = axios as jest.Mocked; jest.mock('../src/toolbox_core/tool', () => ({ ToolboxTool: jest.fn(), })); -const MockedToolboxToolFactory = ToolboxTool as jest.MockedFunction< - typeof ToolboxTool ->; + +const MockedToolboxToolFactory = + ToolboxTool as jest.MockedFunction; jest.mock('../src/toolbox_core/protocol', () => ({ ZodManifestSchema: { @@ -100,35 +121,64 @@ describe('ToolboxClient', () => { }); const setupMocksForSuccessfulLoad = ( - toolDefinition: object, + toolDefinition: { + description: string; + parameters: {name: string; type: string; description: string}[]; + }, overrides: { manifestData?: Partial; - zodParamsSchema?: object; - toolInstance?: object; + zodParamsSchema?: ZodObject; + toolInstance?: Partial; } = {} ) => { const manifestData: ZodManifest = { serverVersion: '1.0.0', tools: {[toolName]: toolDefinition}, - ...overrides.manifestData, // Allow overriding parts of the manifest + ...overrides.manifestData, } as ZodManifest; - const zodParamsSchema = overrides.zodParamsSchema || { - _isMockZodParamSchema: true, - forTool: toolName, - }; - const toolInstance = overrides.toolInstance || { - _isMockTool: true, - loadedName: toolName, - }; + + const zodParamsSchema = + overrides.zodParamsSchema || + createMockZodObject( + toolDefinition.parameters.reduce( + (shapeAccumulator: ZodRawShape, param) => { + shapeAccumulator[param.name] = { + _def: {typeName: 'ZodString'}, + } as unknown as ZodTypeAny; + return shapeAccumulator; + }, + {} as ZodRawShape + ) + ); + + const defaultMockCallable = jest + .fn() + .mockResolvedValue({result: 'mock tool execution'}); + const defaultToolInstance: CallableToolReturnedByFactory = Object.assign( + defaultMockCallable, + { + toolName: toolName, + description: toolDefinition.description, + params: zodParamsSchema, + getName: jest.fn().mockReturnValue(toolName), + getDescription: jest.fn().mockReturnValue(toolDefinition.description), + getParamSchema: jest.fn().mockReturnValue(zodParamsSchema), + } + ); + + const toolInstance = overrides.toolInstance + ? {...defaultToolInstance, ...overrides.toolInstance} + : defaultToolInstance; mockSessionGet.mockResolvedValueOnce({ data: manifestData, } as AxiosResponse); MockedZodManifestSchema.parse.mockReturnValueOnce(manifestData); - MockedCreateZodSchemaFromParams.mockReturnValueOnce( - zodParamsSchema as ZodObject + MockedCreateZodSchemaFromParams.mockReturnValueOnce(zodParamsSchema); + + MockedToolboxToolFactory.mockReturnValueOnce( + toolInstance as CallableToolReturnedByFactory ); - MockedToolboxToolFactory.mockReturnValueOnce(toolInstance as any); return {manifestData, zodParamsSchema, toolInstance}; }; From 1897fc38ef46203f812d6ca0ad438673a385acee Mon Sep 17 00:00:00 2001 From: Twisha Bansal <58483338+twishabansal@users.noreply.github.com> Date: Mon, 26 May 2025 10:09:03 +0530 Subject: [PATCH 57/57] Update client.ts --- packages/toolbox-core/src/toolbox_core/client.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/toolbox-core/src/toolbox_core/client.ts b/packages/toolbox-core/src/toolbox_core/client.ts index ba34c6f..615512d 100644 --- a/packages/toolbox-core/src/toolbox_core/client.ts +++ b/packages/toolbox-core/src/toolbox_core/client.ts @@ -20,6 +20,7 @@ import {logApiError} from './errorUtils'; /** * An asynchronous client for interacting with a Toolbox service. + * Manages an Axios Client Session, if not provided. */ class ToolboxClient { private _baseUrl: string;