From ed477b201a334ab6fe7d5a79fca75521adcbb471 Mon Sep 17 00:00:00 2001 From: "." Date: Mon, 9 Mar 2026 00:42:22 +0800 Subject: [PATCH] test: add unit tests for getHost, getPort, and jsonSchemaToTypebox Add vitest as dev dependency and create comprehensive unit tests covering config resolution and JSON Schema to TypeBox conversion. Export the pure utility functions to make them testable. 15 test cases covering: default/custom config values, all schema types (string, number, integer, boolean, array, object), required vs optional properties, and unknown type fallback behavior. --- index.test.ts | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++ index.ts | 6 +-- package.json | 8 +++- 3 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 index.test.ts diff --git a/index.test.ts b/index.test.ts new file mode 100644 index 0000000..39baa4a --- /dev/null +++ b/index.test.ts @@ -0,0 +1,117 @@ +import { describe, it, expect } from "vitest"; +import { getHost, getPort, jsonSchemaToTypebox } from "./index.js"; +import { Type, Kind, OptionalKind } from "@sinclair/typebox"; + +describe("getHost", () => { + it("returns default host when no config", () => { + expect(getHost()).toBe("127.0.0.1"); + expect(getHost(undefined)).toBe("127.0.0.1"); + }); + + it("returns default host when config has no mcpHost", () => { + expect(getHost({})).toBe("127.0.0.1"); + expect(getHost({ mcpHost: 123 })).toBe("127.0.0.1"); + expect(getHost({ mcpHost: "" })).toBe("127.0.0.1"); + }); + + it("returns custom host from config", () => { + expect(getHost({ mcpHost: "192.168.1.100" })).toBe("192.168.1.100"); + }); +}); + +describe("getPort", () => { + it("returns default port when no config", () => { + expect(getPort()).toBe(9990); + expect(getPort(undefined)).toBe(9990); + }); + + it("returns default port when config has no mcpPort", () => { + expect(getPort({})).toBe(9990); + expect(getPort({ mcpPort: "9991" })).toBe(9990); + }); + + it("returns custom port from config", () => { + expect(getPort({ mcpPort: 8080 })).toBe(8080); + }); +}); + +describe("jsonSchemaToTypebox", () => { + it("returns empty object schema for undefined input", () => { + const schema = jsonSchemaToTypebox(undefined); + expect(schema[Kind]).toBe("Object"); + expect(schema.properties).toEqual({}); + }); + + it("returns empty object schema for schema without properties", () => { + const schema = jsonSchemaToTypebox({ type: "object" }); + expect(schema.properties).toEqual({}); + }); + + it("converts string property", () => { + const schema = jsonSchemaToTypebox({ + properties: { name: { type: "string", description: "A name" } }, + required: ["name"], + }); + expect(schema.properties).toHaveProperty("name"); + expect(schema.properties.name[Kind]).toBe("String"); + expect(schema.properties.name.description).toBe("A name"); + }); + + it("converts number and integer properties", () => { + const schema = jsonSchemaToTypebox({ + properties: { + speed: { type: "number" }, + count: { type: "integer" }, + }, + required: ["speed", "count"], + }); + expect(schema.properties.speed[Kind]).toBe("Number"); + expect(schema.properties.count[Kind]).toBe("Number"); + }); + + it("converts boolean property", () => { + const schema = jsonSchemaToTypebox({ + properties: { enabled: { type: "boolean" } }, + required: ["enabled"], + }); + expect(schema.properties.enabled[Kind]).toBe("Boolean"); + }); + + it("marks properties not in required array as optional", () => { + const schema = jsonSchemaToTypebox({ + properties: { + req: { type: "string" }, + opt: { type: "string" }, + }, + required: ["req"], + }); + // Required property has no Optional symbol + expect(schema.properties.req[OptionalKind]).toBeUndefined(); + // Non-required property has Optional symbol + expect(schema.properties.opt[OptionalKind]).toBe("Optional"); + }); + + it("converts array property", () => { + const schema = jsonSchemaToTypebox({ + properties: { items: { type: "array" } }, + required: ["items"], + }); + expect(schema.properties.items[Kind]).toBe("Array"); + }); + + it("converts object property to Record", () => { + const schema = jsonSchemaToTypebox({ + properties: { metadata: { type: "object" } }, + required: ["metadata"], + }); + expect(schema.properties.metadata[Kind]).toBe("Record"); + }); + + it("defaults unknown types to string", () => { + const schema = jsonSchemaToTypebox({ + properties: { unknown: { type: "foobar" } }, + required: ["unknown"], + }); + expect(schema.properties.unknown[Kind]).toBe("String"); + }); +}); diff --git a/index.ts b/index.ts index bba3593..eec9d62 100644 --- a/index.ts +++ b/index.ts @@ -14,14 +14,14 @@ interface McpToolDef { inputSchema: Record; } -function getHost(pluginConfig?: Record): string { +export function getHost(pluginConfig?: Record): string { if (pluginConfig && typeof pluginConfig.mcpHost === "string" && pluginConfig.mcpHost) { return pluginConfig.mcpHost; } return DEFAULT_HOST; } -function getPort(pluginConfig?: Record): number { +export function getPort(pluginConfig?: Record): number { if (pluginConfig && typeof pluginConfig.mcpPort === "number") { return pluginConfig.mcpPort; } @@ -29,7 +29,7 @@ function getPort(pluginConfig?: Record): number { } /** Convert a JSON Schema properties object into a TypeBox Type.Object schema. */ -function jsonSchemaToTypebox( +export function jsonSchemaToTypebox( inputSchema?: Record, ): ReturnType { if (!inputSchema) { diff --git a/package.json b/package.json index 957a485..205b803 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,13 @@ "description": "Exposes dimos MCP tools to the OpenClaw agent", "type": "module", "dependencies": {}, - "devDependencies": {}, + "scripts": { + "test": "vitest run" + }, + "devDependencies": { + "vitest": "^3.0.0", + "@sinclair/typebox": "^0.34.0" + }, "peerDependencies": { "openclaw": ">=2026.1.26" },