@emmahyde/thinking-patterns
Version:
MCP server combining systematic thinking, mental models, debugging approaches, and stochastic algorithms for comprehensive cognitive pattern support
289 lines (288 loc) • 12.3 kB
JavaScript
/**
* Tests for BaseToolServer abstract class
* Tests validation, error handling, and response formatting
*/
import { z } from 'zod';
import { BaseToolServer } from '../../src/base/BaseToolServer.js';
// Test schema for validation testing
const TestSchema = z.object({
message: z.string(),
count: z.number().positive(),
enabled: z.boolean().optional(),
});
// Concrete implementation for testing
class TestToolServer extends BaseToolServer {
constructor() {
super(TestSchema);
}
handle(validInput) {
return {
result: `Processed: ${validInput.message} (count: ${validInput.count})`,
processed: true,
};
}
}
// Test server that throws an error in handle method
class ErrorThrowingServer extends BaseToolServer {
constructor() {
super(TestSchema);
}
handle(_validInput) {
throw new Error("Intentional error for testing");
}
}
describe('BaseToolServer', () => {
let testServer;
let errorServer;
beforeEach(() => {
testServer = new TestToolServer();
errorServer = new ErrorThrowingServer();
});
describe('constructor', () => {
it('should initialize with provided schema', () => {
expect(testServer).toBeInstanceOf(BaseToolServer);
expect(testServer['schema']).toBe(TestSchema);
});
});
describe('validate method', () => {
it('should validate correct input successfully', () => {
const validInput = {
message: "test message",
count: 5,
enabled: true,
};
const result = testServer['validate'](validInput);
expect(result).toEqual(validInput);
expect(result.message).toBe("test message");
expect(result.count).toBe(5);
expect(result.enabled).toBe(true);
});
it('should validate input without optional fields', () => {
const validInput = {
message: "test message",
count: 3,
};
const result = testServer['validate'](validInput);
expect(result).toEqual(validInput);
expect(result.enabled).toBeUndefined();
});
it('should throw error for missing required fields', () => {
const invalidInput = {
message: "test message",
// missing count
};
expect(() => testServer['validate'](invalidInput)).toThrow();
expect(() => testServer['validate'](invalidInput)).toThrow(/Validation failed/);
});
it('should throw error for invalid field types', () => {
const invalidInput = {
message: 123, // should be string
count: "not a number", // should be number
};
expect(() => testServer['validate'](invalidInput)).toThrow();
expect(() => testServer['validate'](invalidInput)).toThrow(/Validation failed/);
});
it('should throw error for negative count', () => {
const invalidInput = {
message: "test",
count: -1, // should be positive
};
expect(() => testServer['validate'](invalidInput)).toThrow();
expect(() => testServer['validate'](invalidInput)).toThrow(/Validation failed/);
});
it('should provide detailed validation error messages', () => {
const invalidInput = {
message: 123,
count: "invalid",
};
try {
testServer['validate'](invalidInput);
expect.fail('Should have thrown validation error');
}
catch (error) {
expect(error).toBeInstanceOf(Error);
expect(error.message).toContain('Validation failed');
expect(error.message).toContain('message');
expect(error.message).toContain('count');
}
});
});
describe('run method', () => {
it('should process valid input and return success response', () => {
const validInput = {
message: "hello world",
count: 42,
enabled: true,
};
const response = testServer.run(validInput);
expect(response).toHaveProperty('content');
expect(response.isError).toBeUndefined(); // Success responses don't set isError
expect(response.content).toHaveLength(1);
expect(response.content[0]).toHaveProperty('type', 'text');
expect(response.content[0]).toHaveProperty('text');
const resultText = response.content[0].text;
const parsedResult = JSON.parse(resultText);
expect(parsedResult).toHaveProperty('result');
expect(parsedResult).toHaveProperty('processed', true);
expect(parsedResult.result).toContain('hello world');
expect(parsedResult.result).toContain('42');
});
it('should return error response for validation failures', () => {
const invalidInput = {
message: "test",
// missing count
};
const response = testServer.run(invalidInput);
expect(response).toHaveProperty('content');
expect(response).toHaveProperty('isError', true);
expect(response.content).toHaveLength(1);
expect(response.content[0]).toHaveProperty('type', 'text');
const errorText = response.content[0].text;
const parsedError = JSON.parse(errorText);
expect(parsedError).toHaveProperty('error');
expect(parsedError).toHaveProperty('status', 'failed');
expect(parsedError).toHaveProperty('timestamp');
expect(parsedError.error).toContain('Validation failed');
});
it('should return error response for handle method errors', () => {
const validInput = {
message: "test",
count: 1,
};
const response = errorServer.run(validInput);
expect(response).toHaveProperty('content');
expect(response).toHaveProperty('isError', true);
expect(response.content).toHaveLength(1);
const errorText = response.content[0].text;
const parsedError = JSON.parse(errorText);
expect(parsedError).toHaveProperty('error', 'Intentional error for testing');
expect(parsedError).toHaveProperty('status', 'failed');
expect(parsedError).toHaveProperty('timestamp');
});
it('should handle non-Error exceptions', () => {
class StringThrowingServer extends BaseToolServer {
constructor() {
super(TestSchema);
}
handle(_validInput) {
throw "String error"; // Throwing a string instead of Error
}
}
const stringServer = new StringThrowingServer();
const validInput = { message: "test", count: 1 };
const response = stringServer.run(validInput);
expect(response.isError).toBe(true);
const errorText = response.content[0].text;
const parsedError = JSON.parse(errorText);
expect(parsedError.error).toBe("String error");
});
});
describe('formatResponse method', () => {
it('should format response with default implementation', () => {
const result = { result: "test data", processed: true };
const formatted = testServer['formatResponse'](result);
expect(formatted).toHaveLength(1);
expect(formatted[0]).toHaveProperty('type', 'text');
expect(formatted[0]).toHaveProperty('text');
const parsedText = JSON.parse(formatted[0].text);
expect(parsedText).toEqual(result);
});
});
describe('formatError method', () => {
it('should format error with default implementation', () => {
const error = new Error("Test error message");
const formatted = testServer['formatError'](error);
expect(formatted).toHaveLength(1);
expect(formatted[0]).toHaveProperty('type', 'text');
expect(formatted[0]).toHaveProperty('text');
const parsedText = JSON.parse(formatted[0].text);
expect(parsedText).toHaveProperty('error', 'Test error message');
expect(parsedText).toHaveProperty('status', 'failed');
expect(parsedText).toHaveProperty('timestamp');
expect(new Date(parsedText.timestamp)).toBeInstanceOf(Date);
});
});
describe('generic type constraints', () => {
it('should enforce input type constraints through schema', () => {
const customSchema = z.object({
value: z.string(),
});
class CustomServer extends BaseToolServer {
constructor() {
super(customSchema);
}
handle(validInput) {
return { output: validInput.value.toUpperCase() };
}
}
const server = new CustomServer();
const response = server.run({ value: "hello" });
expect(response.isError).toBeUndefined();
const result = JSON.parse(response.content[0].text);
expect(result.output).toBe("HELLO");
});
it('should work with complex nested types', () => {
const complexSchema = z.object({
user: z.object({
name: z.string(),
age: z.number(),
}),
settings: z.array(z.string()),
});
class ComplexServer extends BaseToolServer {
constructor() {
super(complexSchema);
}
handle(validInput) {
return {
summary: `User ${validInput.user.name} has ${validInput.settings.length} settings`,
count: validInput.settings.length,
};
}
}
const server = new ComplexServer();
const input = {
user: { name: "Alice", age: 30 },
settings: ["theme", "language", "notifications"],
};
const response = server.run(input);
expect(response.isError).toBeUndefined();
const result = JSON.parse(response.content[0].text);
expect(result.summary).toContain("Alice");
expect(result.summary).toContain("3 settings");
expect(result.count).toBe(3);
});
});
describe('edge cases and error scenarios', () => {
it('should handle null input', () => {
const response = testServer.run(null);
expect(response.isError).toBe(true);
});
it('should handle undefined input', () => {
const response = testServer.run(undefined);
expect(response.isError).toBe(true);
});
it('should handle empty object input', () => {
const response = testServer.run({});
expect(response.isError).toBe(true);
});
it('should handle very large input objects', () => {
const largeInput = {
message: "x".repeat(10000),
count: 1,
};
const response = testServer.run(largeInput);
expect(response.isError).toBeUndefined();
const result = JSON.parse(response.content[0].text);
expect(result.processed).toBe(true);
});
it('should preserve timestamp format in error responses', () => {
const response = testServer.run({});
expect(response.isError).toBe(true);
const error = JSON.parse(response.content[0].text);
const timestamp = new Date(error.timestamp);
expect(timestamp.getTime()).not.toBeNaN();
expect(Math.abs(Date.now() - timestamp.getTime())).toBeLessThan(1000);
});
});
});